diff --git a/.github/workflows/pd-test-build-deploy.yaml b/.github/workflows/pd-test-build-deploy.yaml index 9f23419da94..306a475aacc 100644 --- a/.github/workflows/pd-test-build-deploy.yaml +++ b/.github/workflows/pd-test-build-deploy.yaml @@ -162,7 +162,7 @@ jobs: OT_PD_MIXPANEL_ID: ${{ secrets.OT_PD_MIXPANEL_ID }} OT_PD_MIXPANEL_DEV_ID: ${{ secrets.OT_PD_MIXPANEL_DEV_ID }} run: | - make -C protocol-designer + make -C protocol-designer NODE_ENV=development - name: 'upload github artifact' uses: actions/upload-artifact@v3 with: @@ -215,4 +215,7 @@ jobs: aws configure set role_arn ${{ secrets.OT_PD_DEPLOY_ROLE }} --profile deploy aws configure set source_profile identity --profile deploy aws s3 sync ./dist s3://sandbox.designer.opentrons.com/${{ env.OT_BRANCH }} --acl=public-read --profile=deploy + # invalidate both sandbox.opentrons.com and www.sandbox.opentrons.com cloudfront caches + aws cloudfront create-invalidation --distribution-id ${{ secrets.PD_CLOUDFRONT_SANDBOX_DISTRIBUTION_ID }} --paths "/*" --profile deploy + aws cloudfront create-invalidation --distribution-id ${{ secrets.PD_CLOUDFRONT_SANDBOX_WWW_DISTRIBUTION_ID }} --paths "/*" --profile deploy shell: bash diff --git a/.github/workflows/react-api-client-test.yaml b/.github/workflows/react-api-client-test.yaml index a8f5ed959b2..2bccadae770 100644 --- a/.github/workflows/react-api-client-test.yaml +++ b/.github/workflows/react-api-client-test.yaml @@ -32,7 +32,7 @@ env: jobs: js-unit-test: - name: 'react-api-client unit tests' + name: 'api-client and react-api-client unit tests' timeout-minutes: 30 runs-on: 'ubuntu-22.04' steps: @@ -59,8 +59,8 @@ jobs: npm config set cache ./.npm-cache yarn config set cache-folder ./.yarn-cache make setup-js - - name: 'run react-api-client unit tests' - run: make -C react-api-client test-cov + - name: 'run api-client and react-api-client unit tests' + run: make -C api-client test-cov - name: 'Upload coverage report' uses: codecov/codecov-action@v3 with: diff --git a/abr-testing/Pipfile.lock b/abr-testing/Pipfile.lock index ff84e2bfab3..05e3c72eeda 100644 --- a/abr-testing/Pipfile.lock +++ b/abr-testing/Pipfile.lock @@ -2,7 +2,6 @@ "_meta": { "hash": { "sha256": "b82b82f6cc192a520ccaa5f2c94a2dda16993ef31ebd9e140f85f80b6a96cc6a" - }, "pipfile-spec": 6, "requires": { @@ -21,93 +20,117 @@ "editable": true, "path": "." }, + "aiohappyeyeballs": { + "hashes": [ + "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2", + "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd" + ], + "markers": "python_version >= '3.8'", + "version": "==2.4.0" + }, "aiohttp": { "hashes": [ - "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8", - "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c", - "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475", - "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed", - "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf", - "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372", - "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81", - "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f", - "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1", - "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd", - "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a", - "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb", - "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46", - "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de", - "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78", - "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c", - "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771", - "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb", - "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430", - "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233", - "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156", - "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9", - "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59", - "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888", - "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c", - "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c", - "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da", - "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424", - "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2", - "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb", - "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8", - "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a", - "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10", - "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0", - "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09", - "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031", - "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4", - "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3", - "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa", - "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a", - "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe", - "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a", - "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2", - "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1", - "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323", - "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b", - "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b", - "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106", - "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac", - "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6", - "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832", - "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75", - "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6", - "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d", - "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72", - "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db", - "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a", - "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da", - "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678", - "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b", - "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24", - "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed", - "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f", - "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e", - "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58", - "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a", - "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342", - "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558", - "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2", - "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551", - "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595", - "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee", - "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11", - "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d", - "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7", - "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f" + "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277", + "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1", + "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe", + "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb", + "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca", + "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91", + "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972", + "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a", + "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3", + "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa", + "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77", + "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b", + "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8", + "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599", + "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc", + "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf", + "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511", + "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699", + "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487", + "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987", + "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff", + "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db", + "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022", + "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce", + "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a", + "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5", + "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7", + "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820", + "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf", + "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e", + "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf", + "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5", + "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6", + "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6", + "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91", + "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3", + "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a", + "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d", + "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088", + "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc", + "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f", + "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75", + "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471", + "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e", + "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697", + "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092", + "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69", + "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3", + "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32", + "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589", + "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178", + "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92", + "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2", + "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e", + "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058", + "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857", + "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1", + "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6", + "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22", + "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0", + "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b", + "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57", + "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f", + "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e", + "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16", + "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1", + "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f", + "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6", + "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04", + "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae", + "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d", + "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b", + "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f", + "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862", + "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689", + "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c", + "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683", + "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef", + "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f", + "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12", + "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73", + "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061", + "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072", + "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11", + "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691", + "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77", + "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385", + "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172", + "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569", + "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f", + "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5" ], "markers": "python_version >= '3.8'", - "version": "==3.9.5" + "version": "==3.10.5" }, "aionotify": { "hashes": [ "sha256:25816a9eef030c774beaee22189a24e29bc43f81cebe574ef723851eaf89ddee", "sha256:9651e1373873c75786101330e302e114f85b6e8b5ad70b491497c8b3609a8449" ], + "markers": "python_version >= '3.8'", "version": "==0.3.1" }, "aiosignal": { @@ -136,27 +159,27 @@ }, "attrs": { "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" ], "markers": "python_version >= '3.7'", - "version": "==23.2.0" + "version": "==24.2.0" }, "cachetools": { "hashes": [ - "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", - "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105" + "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", + "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a" ], "markers": "python_version >= '3.7'", - "version": "==5.3.3" + "version": "==5.5.0" }, "certifi": { "hashes": [ - "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", - "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.6.2" + "version": "==2024.8.30" }, "charset-normalizer": { "hashes": [ @@ -272,11 +295,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", - "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.1" + "version": "==1.2.2" }, "frozenlist": { "hashes": [ @@ -363,28 +386,28 @@ }, "google-api-core": { "hashes": [ - "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125", - "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd" + "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4", + "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f" ], "markers": "python_version >= '3.7'", - "version": "==2.19.1" + "version": "==2.19.2" }, "google-api-python-client": { "hashes": [ - "sha256:4a8f0bea651a212997cc83c0f271fc86f80ef93d1cee9d84de7dfaeef2a858b6", - "sha256:ba05d60f6239990b7994f6328f17bb154c602d31860fb553016dc9f8ce886945" + "sha256:8b84dde11aaccadc127e4846f5cd932331d804ea324e353131595e3f25376e97", + "sha256:d74da1358f3f2d63daf3c6f26bd96d89652051183bc87cf10a56ceb2a70beb50" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.134.0" + "version": "==2.145.0" }, "google-auth": { "hashes": [ - "sha256:8df7da660f62757388b8a7f249df13549b3373f24388cb5d2f1dd91cc18180b5", - "sha256:ab630a1320f6720909ad76a7dbdb6841cdf5c66b328d690027e4867bdfb16688" + "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", + "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc" ], "markers": "python_version >= '3.7'", - "version": "==2.30.0" + "version": "==2.34.0" }, "google-auth-httplib2": { "hashes": [ @@ -395,19 +418,19 @@ }, "google-auth-oauthlib": { "hashes": [ - "sha256:292d2d3783349f2b0734a0a0207b1e1e322ac193c2c09d8f7c613fb7cc501ea8", - "sha256:297c1ce4cb13a99b5834c74a1fe03252e1e499716718b190f56bcb9c4abc4faf" + "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f", + "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263" ], "markers": "python_version >= '3.6'", - "version": "==1.2.0" + "version": "==1.2.1" }, "googleapis-common-protos": { "hashes": [ - "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945", - "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87" + "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", + "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" ], "markers": "python_version >= '3.7'", - "version": "==1.63.2" + "version": "==1.65.0" }, "gspread": { "hashes": [ @@ -433,19 +456,11 @@ }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" - ], - "markers": "python_version >= '3.5'", - "version": "==3.7" - }, - "joblib": { - "hashes": [ - "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", - "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e" + "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" ], - "markers": "python_version >= '3.8'", - "version": "==1.4.2" + "markers": "python_version >= '3.6'", + "version": "==3.8" }, "jsonschema": { "hashes": [ @@ -457,99 +472,101 @@ }, "multidict": { "hashes": [ - "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", - "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", - "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", - "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", - "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", - "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", - "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", - "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", - "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", - "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", - "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", - "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", - "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", - "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", - "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", - "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", - "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", - "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", - "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", - "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", - "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", - "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", - "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", - "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", - "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", - "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", - "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", - "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", - "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", - "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", - "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", - "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", - "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", - "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", - "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", - "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", - "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", - "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", - "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", - "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", - "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", - "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", - "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", - "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", - "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", - "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", - "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", - "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", - "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", - "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", - "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", - "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", - "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", - "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", - "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", - "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", - "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", - "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", - "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", - "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", - "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", - "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", - "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", - "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", - "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", - "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", - "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", - "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", - "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", - "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", - "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", - "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", - "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", - "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", - "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", - "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", - "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", - "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", - "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", - "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", - "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", - "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", - "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", - "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", - "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", - "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", - "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", - "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", - "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", - "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" + "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", + "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", + "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", + "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", + "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", + "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", + "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", + "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", + "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", + "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", + "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6", + "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", + "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", + "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2", + "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", + "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", + "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef", + "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", + "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", + "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", + "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6", + "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", + "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478", + "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", + "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", + "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", + "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", + "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", + "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", + "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", + "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", + "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", + "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", + "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", + "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", + "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", + "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", + "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", + "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", + "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2", + "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", + "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", + "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", + "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", + "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", + "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", + "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492", + "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", + "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", + "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", + "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", + "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", + "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc", + "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", + "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", + "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", + "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", + "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", + "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", + "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", + "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", + "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", + "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", + "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd", + "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", + "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", + "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", + "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", + "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", + "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", + "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4", + "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", + "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", + "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", + "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d", + "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a", + "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", + "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", + "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", + "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", + "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", + "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", + "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392", + "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167", + "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", + "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", + "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", + "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", + "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", + "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd", + "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", + "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db" ], - "markers": "python_version >= '3.7'", - "version": "==6.0.5" + "markers": "python_version >= '3.8'", + "version": "==6.1.0" }, "numpy": { "hashes": [ @@ -590,7 +607,7 @@ "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" ], - "markers": "python_version < '3.12' and python_version >= '3.9'", + "markers": "python_version >= '3.9'", "version": "==1.26.4" }, "oauth2client": { @@ -675,14 +692,13 @@ }, "pandas-stubs": { "hashes": [ - "sha256:2dcc86e8fa6ea41535a4561c1f08b3942ba5267b464eff2e99caeee66f9e4cd1", - "sha256:e08ce7f602a4da2bff5a67475ba881c39f2a4d4f7fccc1cba57c6f35a379c6c0" + "sha256:3c0951a2c3e45e3475aed9d80b7147ae82f176b9e42e9fb321cfdebf3d411b3d", + "sha256:e230f5fa4065f9417804f4d65cd98f86c002efcc07933e8abcd48c3fad9c30a2" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==2.2.2.240603" + "markers": "python_version >= '3.10'", + "version": "==2.2.2.240909" }, - "proto-plus": { "hashes": [ "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445", @@ -693,93 +709,91 @@ }, "protobuf": { "hashes": [ - "sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505", - "sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b", - "sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38", - "sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863", - "sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470", - "sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6", - "sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce", - "sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca", - "sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5", - "sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e", - "sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714" + "sha256:018db9056b9d75eb93d12a9d35120f97a84d9a919bcab11ed56ad2d399d6e8dd", + "sha256:510ed78cd0980f6d3218099e874714cdf0d8a95582e7b059b06cabad855ed0a0", + "sha256:532627e8fdd825cf8767a2d2b94d77e874d5ddb0adefb04b237f7cc296748681", + "sha256:6206afcb2d90181ae8722798dcb56dc76675ab67458ac24c0dd7d75d632ac9bd", + "sha256:66c3edeedb774a3508ae70d87b3a19786445fe9a068dd3585e0cefa8a77b83d0", + "sha256:6d7cc9e60f976cf3e873acb9a40fed04afb5d224608ed5c1a105db4a3f09c5b6", + "sha256:853db610214e77ee817ecf0514e0d1d052dff7f63a0c157aa6eabae98db8a8de", + "sha256:d001a73c8bc2bf5b5c1360d59dd7573744e163b3607fa92788b7f3d5fefbd9a5", + "sha256:dde74af0fa774fa98892209992295adbfb91da3fa98c8f67a88afe8f5a349add", + "sha256:dde9fcaa24e7a9654f4baf2a55250b13a5ea701493d904c54069776b99a8216b", + "sha256:eef7a8a2f4318e2cb2dee8666d26e58eaf437c14788f3a2911d0c3da40405ae8" ], "markers": "python_version >= '3.8'", - "version": "==5.27.2" + "version": "==5.28.0" }, "pyasn1": { "hashes": [ - "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", - "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" + "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" ], "markers": "python_version >= '3.8'", - "version": "==0.6.0" + "version": "==0.6.1" }, "pyasn1-modules": { "hashes": [ - "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", - "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" + "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" ], "markers": "python_version >= '3.8'", - "version": "==0.4.0" + "version": "==0.4.1" }, "pydantic": { "hashes": [ - "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f", - "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc", - "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b", - "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b", - "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b", - "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e", - "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3", - "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7", - "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227", - "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f", - "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6", - "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab", - "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad", - "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076", - "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681", - "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54", - "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb", - "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7", - "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe", - "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b", - "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab", - "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d", - "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0", - "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75", - "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741", - "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63", - "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd", - "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33", - "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815", - "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7", - "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a", - "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655", - "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773", - "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c", - "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7", - "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3", - "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768", - "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d", - "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688", - "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f", - "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e", - "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991", - "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a" + "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620", + "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82", + "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62", + "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c", + "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c", + "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682", + "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048", + "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b", + "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03", + "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f", + "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a", + "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1", + "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe", + "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33", + "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f", + "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518", + "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485", + "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f", + "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec", + "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70", + "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86", + "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf", + "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d", + "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588", + "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481", + "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9", + "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3", + "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab", + "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7", + "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a", + "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0", + "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc", + "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861", + "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357", + "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a", + "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3", + "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80", + "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02", + "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b", + "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5", + "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2", + "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890", + "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f" ], "markers": "python_version >= '3.7'", - "version": "==1.10.17" + "version": "==1.10.18" }, "pyparsing": { "hashes": [ - "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", - "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" + "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", + "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" ], "markers": "python_version >= '3.1'", - "version": "==3.1.2" + "version": "==3.1.4" }, "pyrsistent": { "hashes": [ @@ -844,10 +858,18 @@ }, "pytz": { "hashes": [ - "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", - "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", + "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725" ], - "version": "==2024.1" + "version": "==2024.2" + }, + "pyusb": { + "hashes": [ + "sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36", + "sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==1.2.1" }, "pywin32": { "hashes": [ @@ -893,72 +915,13 @@ "markers": "python_version >= '3.6' and python_version < '4'", "version": "==4.9" }, - "scikit-learn": { - "hashes": [ - "sha256:057b991ac64b3e75c9c04b5f9395eaf19a6179244c089afdebaad98264bff37c", - "sha256:118a8d229a41158c9f90093e46b3737120a165181a1b58c03461447aa4657415", - "sha256:12e40ac48555e6b551f0a0a5743cc94cc5a765c9513fe708e01f0aa001da2801", - "sha256:174beb56e3e881c90424e21f576fa69c4ffcf5174632a79ab4461c4c960315ac", - "sha256:1b94d6440603752b27842eda97f6395f570941857456c606eb1d638efdb38184", - "sha256:1f77547165c00625551e5c250cefa3f03f2fc92c5e18668abd90bfc4be2e0bff", - "sha256:261fe334ca48f09ed64b8fae13f9b46cc43ac5f580c4a605cbb0a517456c8f71", - "sha256:2a65af2d8a6cce4e163a7951a4cfbfa7fceb2d5c013a4b593686c7f16445cf9d", - "sha256:2c75ea812cd83b1385bbfa94ae971f0d80adb338a9523f6bbcb5e0b0381151d4", - "sha256:40fb7d4a9a2db07e6e0cae4dc7bdbb8fada17043bac24104d8165e10e4cff1a2", - "sha256:460806030c666addee1f074788b3978329a5bfdc9b7d63e7aad3f6d45c67a210", - "sha256:47132440050b1c5beb95f8ba0b2402bbd9057ce96ec0ba86f2f445dd4f34df67", - "sha256:4c0c56c3005f2ec1db3787aeaabefa96256580678cec783986836fc64f8ff622", - "sha256:789e3db01c750ed6d496fa2db7d50637857b451e57bcae863bff707c1247bef7", - "sha256:855fc5fa8ed9e4f08291203af3d3e5fbdc4737bd617a371559aaa2088166046e", - "sha256:a03b09f9f7f09ffe8c5efffe2e9de1196c696d811be6798ad5eddf323c6f4d40", - "sha256:a3a10e1d9e834e84d05e468ec501a356226338778769317ee0b84043c0d8fb06", - "sha256:a90c5da84829a0b9b4bf00daf62754b2be741e66b5946911f5bdfaa869fcedd6", - "sha256:d82c2e573f0f2f2f0be897e7a31fcf4e73869247738ab8c3ce7245549af58ab8", - "sha256:df8ccabbf583315f13160a4bb06037bde99ea7d8211a69787a6b7c5d4ebb6fc3", - "sha256:f405c4dae288f5f6553b10c4ac9ea7754d5180ec11e296464adb5d6ac68b6ef5" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==1.5.0" - }, - "scipy": { - "hashes": [ - "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", - "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", - "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", - "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", - "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", - "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", - "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", - "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", - "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", - "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", - "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", - "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", - "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", - "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", - "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", - "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", - "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", - "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", - "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", - "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", - "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", - "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", - "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", - "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", - "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f" - ], - "markers": "python_version >= '3.9'", - "version": "==1.13.1" - }, "setuptools": { "hashes": [ - "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650", - "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95" + "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", + "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6" ], "markers": "python_version >= '3.8'", - "version": "==70.1.1" + "version": "==74.1.2" }, "six": { "hashes": [ @@ -970,12 +933,12 @@ }, "slack-sdk": { "hashes": [ - "sha256:001a4013698d3f244645add49c80adf8addc3a6bf633193848f7cbae3d387e0b", - "sha256:42d1c95f7159887ddb4841d461fbe7ab0c48e4968f3cd44eaaa792cf109f4425" + "sha256:af8fc4ef1d1cbcecd28d01acf6955a3bb5b13d56f0a43a1b1c7e3b212cc5ec5b", + "sha256:f35e85f2847e6c25cf7c2d1df206ca0ad75556263fb592457bf03cca68ef64bb" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.30.0" + "version": "==3.32.0" }, "slackclient": { "hashes": [ @@ -1001,14 +964,6 @@ ], "version": "==0.4.15" }, - "threadpoolctl": { - "hashes": [ - "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", - "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467" - ], - "markers": "python_version >= '3.8'", - "version": "==3.5.0" - }, "types-httplib2": { "hashes": [ "sha256:1eda99fea18ec8a1dc1a725ead35b889d0836fec1b11ae6f1fe05440724c1d15", @@ -1136,99 +1091,101 @@ }, "yarl": { "hashes": [ - "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", - "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", - "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", - "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", - "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", - "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", - "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", - "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", - "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", - "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", - "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", - "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", - "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", - "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", - "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", - "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", - "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", - "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", - "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", - "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", - "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", - "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", - "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", - "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", - "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", - "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", - "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", - "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", - "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", - "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", - "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", - "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", - "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", - "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", - "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", - "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", - "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", - "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", - "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", - "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", - "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", - "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", - "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", - "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", - "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", - "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", - "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", - "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", - "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", - "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", - "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", - "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", - "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", - "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", - "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", - "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", - "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", - "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", - "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", - "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", - "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", - "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", - "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", - "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", - "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", - "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", - "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", - "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", - "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", - "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", - "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", - "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", - "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", - "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", - "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", - "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", - "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", - "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", - "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", - "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", - "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", - "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", - "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", - "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", - "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", - "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", - "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", - "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", - "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", - "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" + "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49", + "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867", + "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520", + "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a", + "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14", + "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a", + "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93", + "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05", + "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937", + "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74", + "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b", + "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420", + "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639", + "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089", + "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53", + "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e", + "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c", + "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e", + "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe", + "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a", + "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366", + "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63", + "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9", + "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145", + "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf", + "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc", + "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5", + "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff", + "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d", + "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b", + "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00", + "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad", + "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92", + "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998", + "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91", + "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b", + "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a", + "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5", + "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff", + "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367", + "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa", + "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413", + "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4", + "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45", + "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6", + "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5", + "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df", + "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c", + "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318", + "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591", + "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38", + "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8", + "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e", + "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804", + "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec", + "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6", + "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870", + "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83", + "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d", + "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f", + "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909", + "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269", + "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26", + "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b", + "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2", + "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7", + "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd", + "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68", + "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0", + "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786", + "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da", + "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc", + "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447", + "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239", + "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0", + "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84", + "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e", + "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef", + "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e", + "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82", + "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675", + "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26", + "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979", + "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46", + "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4", + "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff", + "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27", + "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c", + "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7", + "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265", + "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79", + "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd" ], - "markers": "python_version >= '3.7'", - "version": "==1.9.4" + "markers": "python_version >= '3.8'", + "version": "==1.11.1" } }, "develop": { @@ -1241,11 +1198,11 @@ }, "attrs": { "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" ], "markers": "python_version >= '3.7'", - "version": "==23.2.0" + "version": "==24.2.0" }, "black": { "hashes": [ @@ -1279,19 +1236,19 @@ }, "cachetools": { "hashes": [ - "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", - "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105" + "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", + "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a" ], "markers": "python_version >= '3.7'", - "version": "==5.3.3" + "version": "==5.5.0" }, "certifi": { "hashes": [ - "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", - "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.6.2" + "version": "==2024.8.30" }, "charset-normalizer": { "hashes": [ @@ -1407,61 +1364,81 @@ }, "coverage": { "hashes": [ - "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f", - "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d", - "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747", - "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f", - "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d", - "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f", - "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47", - "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e", - "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba", - "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c", - "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b", - "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4", - "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7", - "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555", - "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233", - "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace", - "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805", - "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136", - "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4", - "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d", - "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806", - "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99", - "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8", - "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b", - "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5", - "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da", - "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0", - "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078", - "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f", - "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029", - "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353", - "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638", - "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9", - "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f", - "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7", - "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3", - "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e", - "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016", - "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088", - "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4", - "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882", - "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7", - "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53", - "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d", - "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080", - "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5", - "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d", - "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c", - "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8", - "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633", - "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9", - "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c" + "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", + "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", + "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", + "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", + "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", + "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", + "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", + "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", + "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", + "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", + "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", + "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", + "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", + "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", + "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", + "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", + "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", + "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", + "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", + "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", + "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", + "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", + "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", + "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", + "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", + "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", + "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", + "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", + "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", + "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", + "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", + "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", + "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", + "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", + "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", + "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", + "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", + "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", + "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", + "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", + "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", + "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", + "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", + "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", + "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", + "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", + "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", + "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", + "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", + "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", + "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", + "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", + "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", + "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", + "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", + "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", + "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", + "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", + "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", + "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", + "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", + "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", + "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", + "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", + "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", + "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", + "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", + "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", + "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", + "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", + "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", + "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc" ], "markers": "python_version >= '3.8'", - "version": "==7.5.4" + "version": "==7.6.1" }, "flake8": { "hashes": [ @@ -1500,37 +1477,37 @@ }, "google-api-core": { "hashes": [ - "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125", - "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd" + "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4", + "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f" ], "markers": "python_version >= '3.7'", - "version": "==2.19.1" + "version": "==2.19.2" }, "google-api-python-client": { "hashes": [ - "sha256:4a8f0bea651a212997cc83c0f271fc86f80ef93d1cee9d84de7dfaeef2a858b6", - "sha256:ba05d60f6239990b7994f6328f17bb154c602d31860fb553016dc9f8ce886945" + "sha256:8b84dde11aaccadc127e4846f5cd932331d804ea324e353131595e3f25376e97", + "sha256:d74da1358f3f2d63daf3c6f26bd96d89652051183bc87cf10a56ceb2a70beb50" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.134.0" + "version": "==2.145.0" }, "google-api-python-client-stubs": { "hashes": [ - "sha256:0614b0cef5beac43e6ab02418f07e64ee66dc99ae4e377d54a155ac261533987", - "sha256:f3b38b46f7b5cf4f6e7cc63ca554a2d23096d49c841f38b9ea553a5237074b56" + "sha256:148e16613e070969727f39691e23a73cdb87c65a4fc8133abd4c41d17b80b313", + "sha256:3c1f9f2a7cac8d1e9a7e84ed24e6c29cf4c643b0f94e39ed09ac1b7e91ab239a" ], "index": "pypi", "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.26.0" + "version": "==1.27.0" }, "google-auth": { "hashes": [ - "sha256:8df7da660f62757388b8a7f249df13549b3373f24388cb5d2f1dd91cc18180b5", - "sha256:ab630a1320f6720909ad76a7dbdb6841cdf5c66b328d690027e4867bdfb16688" + "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", + "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc" ], "markers": "python_version >= '3.7'", - "version": "==2.30.0" + "version": "==2.34.0" }, "google-auth-httplib2": { "hashes": [ @@ -1541,11 +1518,11 @@ }, "googleapis-common-protos": { "hashes": [ - "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945", - "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87" + "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", + "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" ], "markers": "python_version >= '3.7'", - "version": "==1.63.2" + "version": "==1.65.0" }, "httplib2": { "hashes": [ @@ -1558,11 +1535,11 @@ }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.8" }, "iniconfig": { "hashes": [ @@ -1639,11 +1616,11 @@ }, "platformdirs": { "hashes": [ - "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", - "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" + "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c", + "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617" ], "markers": "python_version >= '3.8'", - "version": "==4.2.2" + "version": "==4.3.2" }, "pluggy": { "hashes": [ @@ -1663,20 +1640,20 @@ }, "protobuf": { "hashes": [ - "sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505", - "sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b", - "sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38", - "sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863", - "sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470", - "sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6", - "sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce", - "sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca", - "sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5", - "sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e", - "sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714" + "sha256:018db9056b9d75eb93d12a9d35120f97a84d9a919bcab11ed56ad2d399d6e8dd", + "sha256:510ed78cd0980f6d3218099e874714cdf0d8a95582e7b059b06cabad855ed0a0", + "sha256:532627e8fdd825cf8767a2d2b94d77e874d5ddb0adefb04b237f7cc296748681", + "sha256:6206afcb2d90181ae8722798dcb56dc76675ab67458ac24c0dd7d75d632ac9bd", + "sha256:66c3edeedb774a3508ae70d87b3a19786445fe9a068dd3585e0cefa8a77b83d0", + "sha256:6d7cc9e60f976cf3e873acb9a40fed04afb5d224608ed5c1a105db4a3f09c5b6", + "sha256:853db610214e77ee817ecf0514e0d1d052dff7f63a0c157aa6eabae98db8a8de", + "sha256:d001a73c8bc2bf5b5c1360d59dd7573744e163b3607fa92788b7f3d5fefbd9a5", + "sha256:dde74af0fa774fa98892209992295adbfb91da3fa98c8f67a88afe8f5a349add", + "sha256:dde9fcaa24e7a9654f4baf2a55250b13a5ea701493d904c54069776b99a8216b", + "sha256:eef7a8a2f4318e2cb2dee8666d26e58eaf437c14788f3a2911d0c3da40405ae8" ], "markers": "python_version >= '3.8'", - "version": "==5.27.2" + "version": "==5.28.0" }, "py": { "hashes": [ @@ -1688,19 +1665,17 @@ }, "pyasn1": { "hashes": [ - "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", - "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" + "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" ], "markers": "python_version >= '3.8'", - "version": "==0.6.0" + "version": "==0.6.1" }, "pyasn1-modules": { "hashes": [ - "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", - "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" + "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" ], "markers": "python_version >= '3.8'", - "version": "==0.4.0" + "version": "==0.4.1" }, "pycodestyle": { "hashes": [ @@ -1728,11 +1703,11 @@ }, "pyparsing": { "hashes": [ - "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", - "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" + "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", + "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" ], "markers": "python_version >= '3.1'", - "version": "==3.1.2" + "version": "==3.1.4" }, "pytest": { "hashes": [ diff --git a/abr-testing/abr_testing/automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py index b45684dcea0..c150eb93f5e 100644 --- a/abr-testing/abr_testing/automation/google_sheets_tool.py +++ b/abr-testing/abr_testing/automation/google_sheets_tool.py @@ -117,7 +117,7 @@ def batch_delete_rows(self, row_indices: List[int]) -> None: def batch_update_cells( self, data: List[List[Any]], - start_column: str, + start_column_index: Any, start_row: int, sheet_id: str, ) -> None: @@ -132,7 +132,8 @@ def column_letter_to_index(column_letter: str) -> int: requests = [] user_entered_value: Dict[str, Any] = {} - start_column_index = column_letter_to_index(start_column) - 1 + if type(start_column_index) == str: + start_column_index = column_letter_to_index(start_column_index) - 1 for col_offset, col_values in enumerate(data): column_index = start_column_index + col_offset @@ -223,9 +224,9 @@ def get_sheet_by_name(self, title: str) -> None: ) def token_check(self) -> None: - """Check if still credentials are still logged in.""" - if self.credentials.access_token_expired: - self.gc.login() + """Check if credentials are still valid and refresh if expired.""" + if self.credentials.expired: + self.credentials.refresh() # Refresh the credentials def get_row_index_with_value(self, some_string: str, col_num: int) -> Any: """Find row index of string by looking in specific column.""" diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index 6f81503ec42..d43db612561 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -51,8 +51,12 @@ def issues_on_board(self, project_key: str) -> List[List[Any]]: def match_issues(self, issue_ids: List[List[str]], ticket_summary: str) -> List: """Matches related ticket ID's.""" to_link = [] - error = ticket_summary.split("_")[3] - robot = ticket_summary.split("_")[0] + try: + error = ticket_summary.split("_")[3] + robot = ticket_summary.split("_")[0] + except IndexError: + error = "" + robot = "" # for every issue see if both match, if yes then grab issue ID and add it to a list for issue in issue_ids: summary = issue[1] diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 3bd03cf3e3d..f1fe6ad0c8f 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -65,13 +65,7 @@ def create_data_dictionary( left_pipette = file_results.get("left", "") right_pipette = file_results.get("right", "") extension = file_results.get("extension", "") - ( - num_of_errors, - error_type, - error_code, - error_instrument, - error_level, - ) = read_robot_logs.get_error_info(file_results) + error_dict = read_robot_logs.get_error_info(file_results) all_modules = get_modules(file_results) @@ -99,7 +93,7 @@ def create_data_dictionary( pass # Handle datetime parsing errors if necessary if run_time_min > 0: - row = { + run_row = { "Robot": robot, "Run_ID": run_id, "Protocol_Name": protocol_name, @@ -108,15 +102,13 @@ def create_data_dictionary( "Start_Time": start_time_str, "End_Time": complete_time_str, "Run_Time (min)": run_time_min, - "Errors": num_of_errors, - "Error_Code": error_code, - "Error_Type": error_type, - "Error_Instrument": error_instrument, - "Error_Level": error_level, + } + instrument_row = { "Left Mount": left_pipette, "Right Mount": right_pipette, "Extension": extension, } + row = {**run_row, **error_dict, **instrument_row} tc_dict = read_robot_logs.thermocycler_commands(file_results) hs_dict = read_robot_logs.hs_commands(file_results) tm_dict = read_robot_logs.temperature_module_commands(file_results) @@ -128,7 +120,11 @@ def create_data_dictionary( "Average Temp (oC)": "", "Average RH(%)": "", } - row_for_lpc = {**row, **all_modules, **notes} + row_for_lpc = { + **row, + **all_modules, + **notes, + } row_2 = { **row, **all_modules, diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 76a3f09ec18..9f87f7d4c46 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -62,7 +62,9 @@ def compare_current_trh_to_average( df_all_run_data["Start_Time"] = pd.to_datetime( df_all_run_data["Start_Time"], format="mixed", utc=True ).dt.tz_localize(None) - df_all_run_data["Errors"] = pd.to_numeric(df_all_run_data["Errors"]) + df_all_run_data["Run Ending Error"] = pd.to_numeric( + df_all_run_data["Run Ending Error"] + ) df_all_run_data["Average Temp (oC)"] = pd.to_numeric( df_all_run_data["Average Temp (oC)"] ) @@ -70,7 +72,7 @@ def compare_current_trh_to_average( (df_all_run_data["Robot"] == robot) & (df_all_run_data["Start_Time"] >= weeks_ago_3) & (df_all_run_data["Start_Time"] <= start_time) - & (df_all_run_data["Errors"] < 1) + & (df_all_run_data["Run Ending Error"] < 1) & (df_all_run_data["Average Temp (oC)"] > 1) ) @@ -122,7 +124,7 @@ def compare_lpc_to_historical_data( & (df_lpc_data["Robot"] == robot) & (df_lpc_data["Module"] == labware_dict["Module"]) & (df_lpc_data["Adapter"] == labware_dict["Adapter"]) - & (df_lpc_data["Errors"] < 1) + & (df_lpc_data["Run Ending Error"] < 1) ] # Converts coordinates to floats and finds averages. x_float = [float(value) for value in relevant_lpc["X"]] @@ -330,18 +332,17 @@ def get_run_error_info_from_robot( ip, results, storage_directory ) # Error Printout - ( - num_of_errors, - error_type, - error_code, - error_instrument, - error_level, - ) = read_robot_logs.get_error_info(results) + error_dict = read_robot_logs.get_error_info(results) + error_level = error_dict["Error_Level"] + error_type = error_dict["Error_Type"] + error_code = error_dict["Error_Code"] + error_instrument = error_dict["Error_Instrument"] # JIRA Ticket Fields + failure_level = "Level " + str(error_level) + " Failure" components = [failure_level, "Flex-RABR"] - components = match_error_to_component("RABR", error_type, components) + components = match_error_to_component("RABR", str(error_type), components) print(components) affects_version = results["API_Version"] parent = results.get("robot_name", "") diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 740adbf0cb6..96182609c49 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -76,6 +76,36 @@ def command_time(command: Dict[str, str]) -> float: return start_to_complete +def count_command_in_run_data( + commands: List[Dict[str, Any]], command_of_interest: str, find_avg_time: bool +) -> Tuple[int, float]: + """Count number of times command occurs in a run.""" + total_command = 0 + total_time = 0.0 + for command in commands: + command_type = command["commandType"] + if command_type == command_of_interest: + total_command += 1 + if find_avg_time: + started_at = command.get("startedAt", "") + completed_at = command.get("completedAt", "") + + if started_at and completed_at: + try: + start_time = datetime.strptime( + started_at, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + end_time = datetime.strptime( + completed_at, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + total_time += (end_time - start_time).total_seconds() + except ValueError: + # Handle case where date parsing fails + pass + avg_time = total_time / total_command if total_command > 0 else 0.0 + return total_command, avg_time + + def instrument_commands(file_results: Dict[str, Any]) -> Dict[str, float]: """Count number of pipette and gripper commands per run.""" pipettes = file_results.get("pipettes", "") @@ -89,6 +119,7 @@ def instrument_commands(file_results: Dict[str, Any]) -> Dict[str, float]: right_pipette_id = "" left_pipette_id = "" gripper_pickups = 0.0 + avg_liquid_probe_time_sec = 0.0 # Match pipette mount to id for pipette in pipettes: if pipette["mount"] == "right": @@ -120,6 +151,9 @@ def instrument_commands(file_results: Dict[str, Any]) -> Dict[str, float]: and command["params"]["strategy"] == "usingGripper" ): gripper_pickups += 1 + liquid_probes, avg_liquid_probe_time_sec = count_command_in_run_data( + commandData, "liquidProbe", True + ) pipette_dict = { "Left Pipette Total Tip Pick Up(s)": left_tip_pick_up, "Left Pipette Total Aspirates": left_aspirate, @@ -128,6 +162,8 @@ def instrument_commands(file_results: Dict[str, Any]) -> Dict[str, float]: "Right Pipette Total Aspirates": right_aspirate, "Right Pipette Total Dispenses": right_dispense, "Gripper Pick Ups": gripper_pickups, + "Total Liquid Probes": liquid_probes, + "Average Liquid Probe Time (sec)": avg_liquid_probe_time_sec, } return pipette_dict @@ -362,50 +398,58 @@ def create_abr_data_sheet( return sheet_location -def get_error_info(file_results: Dict[str, Any]) -> Tuple[int, str, str, str, str]: +def get_error_info(file_results: Dict[str, Any]) -> Dict[str, Any]: """Determines if errors exist in run log and documents them.""" - error_levels = [] - error_level = "" # Read error levels file with open(ERROR_LEVELS_PATH, "r") as error_file: - error_levels = list(csv.reader(error_file)) - num_of_errors = len(file_results["errors"]) - if num_of_errors == 0: - error_type = "" - error_code = "" - error_instrument = "" - error_level = "" - return 0, error_type, error_code, error_instrument, error_level + error_levels = {row[1]: row[4] for row in csv.reader(error_file)} + # Initialize Variables + recoverable_errors: Dict[str, int] = dict() + total_recoverable_errors = 0 + end_run_errors = len(file_results["errors"]) commands_of_run: List[Dict[str, Any]] = file_results.get("commands", []) + error_recovery = file_results.get("hasEverEnteredErrorRecovery", False) + # Count recoverable errors + if error_recovery: + for command in commands_of_run: + error_info = command.get("error", {}) + if error_info.get("isDefined"): + total_recoverable_errors += 1 + error_type = error_info.get("errorType", "") + recoverable_errors[error_type] = ( + recoverable_errors.get(error_type, 0) + 1 + ) + # Get run-ending error info try: - run_command_error: Dict[str, Any] = commands_of_run[-1] - error_str: int = len(run_command_error.get("error", "")) - except IndexError: - error_str = 0 - if error_str > 1: - error_type = run_command_error["error"].get("errorType", "") + run_command_error = commands_of_run[-1]["error"] + error_type = run_command_error.get("errorType", "") if error_type == "PythonException": - # Reassign error_type to be more descriptive - error_type = run_command_error.get("detail", "").split(":")[0] - error_code = run_command_error["error"].get("errorCode", "") + error_type = commands_of_run[-1].get("detail", "").split(":")[0] + error_code = run_command_error.get("errorCode", "") + error_instrument = run_command_error.get("errorInfo", {}).get( + "node", run_command_error.get("errorInfo", {}).get("port", "") + ) + except (IndexError, KeyError): try: - # Instrument Error - error_instrument = run_command_error["error"]["errorInfo"]["node"] - except KeyError: - # Module - error_instrument = run_command_error["error"]["errorInfo"].get("port", "") - else: - error_type = file_results["errors"][0]["errorType"] - error_code = file_results["errors"][0]["errorCode"] - error_instrument = file_results["errors"][0]["detail"] - for error in error_levels: - code_error = error[1] - if code_error == error_code: - error_level = error[4] - if len(error_level) < 1: - error_level = str(4) - - return num_of_errors, error_type, error_code, error_instrument, error_level + error_details = file_results.get("errors", [{}])[0] + except IndexError: + error_details = {} + error_type = error_details.get("errorType", "") + error_code = error_details.get("errorCode", "") + error_instrument = error_details.get("detail", "") + # Determine error level + error_level = error_levels.get(error_code, "4") + # Create dictionary with all error descriptions + error_dict = { + "Total Recoverable Error(s)": total_recoverable_errors, + "Recoverable Error(s) Description": recoverable_errors, + "Run Ending Error": end_run_errors, + "Error_Code": error_code, + "Error_Type": error_type, + "Error_Instrument": error_instrument, + "Error_Level": error_level, + } + return error_dict def write_to_local_and_google_sheet( @@ -570,10 +614,10 @@ def get_calibration_offsets( def get_logs(storage_directory: str, ip: str) -> List[str]: """Get Robot logs.""" log_types: List[Dict[str, Any]] = [ - {"log type": "api.log", "records": 1000}, + {"log type": "api.log", "records": 10000}, {"log type": "server.log", "records": 10000}, {"log type": "serial.log", "records": 10000}, - {"log type": "touchscreen.log", "records": 1000}, + {"log type": "touchscreen.log", "records": 10000}, ] all_paths = [] for log_type in log_types: diff --git a/abr-testing/abr_testing/tools/sync_abr_sheet.py b/abr-testing/abr_testing/tools/sync_abr_sheet.py index 1bae79cd159..aca116292a8 100644 --- a/abr-testing/abr_testing/tools/sync_abr_sheet.py +++ b/abr-testing/abr_testing/tools/sync_abr_sheet.py @@ -15,6 +15,7 @@ def determine_lifetime(abr_google_sheet: Any) -> None: """Record lifetime % of robot, pipettes, and gripper per run.""" # Get all data headers = abr_google_sheet.get_row(1) + lifetime_index = headers.index("Robot Lifetime (%)") all_google_data = abr_google_sheet.get_all_data(expected_headers=headers) # Convert dictionary to pandas dataframe df_sheet_data = pd.DataFrame.from_dict(all_google_data) @@ -92,7 +93,9 @@ def determine_lifetime(abr_google_sheet: Any) -> None: [right_pipette_percent_lifetime], [gripper_percent_lifetime], ] - abr_google_sheet.batch_update_cells(update_list, "AV", row_num, "0") + abr_google_sheet.batch_update_cells( + update_list, lifetime_index, row_num, "0" + ) print(f"Updated row {row_num} for run: {run_id}") @@ -101,6 +104,10 @@ def compare_run_to_temp_data( ) -> None: """Read ABR Data and compare robot and timestamp columns to temp data.""" row_update = 0 + # Get column number for average temp and rh + headers = google_sheet.get_row(1) + temp_index = headers.index("Average Temp (oC)") + 1 + rh_index = headers.index("Average RH(%)") + 1 for run in abr_data: run_id = run["Run_ID"] try: @@ -129,9 +136,9 @@ def compare_run_to_temp_data( avg_humidity = mean(rel_hums) row_num = google_sheet.get_row_index_with_value(run_id, 2) # Write average temperature - google_sheet.update_cell("Sheet1", row_num, 46, avg_temps) + google_sheet.update_cell("Sheet1", row_num, temp_index, avg_temps) # Write average humidity - google_sheet.update_cell("Sheet1", row_num, 47, avg_humidity) + google_sheet.update_cell("Sheet1", row_num, rh_index, avg_humidity) row_update += 1 # TODO: Write averages to google sheet print(f"Updated row {row_num}.") diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json index cf0293eee21..c30512b818b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json @@ -4889,6 +4889,39 @@ }, "startedAt": "TIMESTAMP", "status": "succeeded" + }, + { + "commandType": "dispense", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "517162d1e8d73c035348a1870a8abc8a", + "notes": [], + "params": { + "flowRate": 160.0, + "labwareId": "UUID", + "pipetteId": "UUID", + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -9.8 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 23.28, + "y": 181.18, + "z": 4.5 + }, + "volume": 20.0 + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" } ], "config": { @@ -4902,7 +4935,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "PartialTipMovementNotAllowedError [line 24]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", + "detail": "PartialTipMovementNotAllowedError [line 26]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Moving to NEST 96 Well Plate 200 µL Flat in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -4911,7 +4944,7 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", + "detail": "Moving to NEST 96 Well Plate 200 µL Flat in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", "errorCode": "2004", "errorInfo": {}, "errorType": "PartialTipMovementNotAllowedError", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json index 02df13c1a33..10ee86bd162 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json @@ -3606,6 +3606,82 @@ }, "startedAt": "TIMESTAMP", "status": "succeeded" + }, + { + "commandType": "dispense", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "ddccee6754fe0092b9c66898d66b79a7", + "notes": [], + "params": { + "flowRate": 160.0, + "labwareId": "UUID", + "pipetteId": "UUID", + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -9.8 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 23.28, + "y": 181.18, + "z": 4.5 + }, + "volume": 20.0 + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "5287b77e909d217f4b05e5006cf9ff25", + "notes": [], + "params": { + "addressableAreaName": "movableTrashA3", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "pipetteId": "UUID" + }, + "result": { + "position": { + "x": 466.25, + "y": 364.0, + "z": 40.0 + } + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "b81364c35c04784c34f571446e64484c", + "notes": [], + "params": { + "pipetteId": "UUID" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" } ], "config": { @@ -3616,29 +3692,7 @@ "protocolType": "python" }, "createdAt": "TIMESTAMP", - "errors": [ - { - "createdAt": "TIMESTAMP", - "detail": "PartialTipMovementNotAllowedError [line 20]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", - "errorCode": "4000", - "errorInfo": {}, - "errorType": "ExceptionInProtocolError", - "id": "UUID", - "isDefined": false, - "wrappedErrors": [ - { - "createdAt": "TIMESTAMP", - "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", - "errorCode": "2004", - "errorInfo": {}, - "errorType": "PartialTipMovementNotAllowedError", - "id": "UUID", - "isDefined": false, - "wrappedErrors": [] - } - ] - } - ], + "errors": [], "files": [ { "name": "Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn.py", @@ -3681,7 +3735,7 @@ "pipetteName": "p1000_96" } ], - "result": "not-ok", + "result": "ok", "robotType": "OT-3 Standard", "runTimeParameters": [] } diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json index a3cf2d44d05..66957b72660 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json @@ -6116,6 +6116,39 @@ }, "startedAt": "TIMESTAMP", "status": "succeeded" + }, + { + "commandType": "dispense", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "e1b16944e3d0ff8ae0a964f7e638c1b3", + "notes": [], + "params": { + "flowRate": 160.0, + "labwareId": "UUID", + "pipetteId": "UUID", + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -9.8 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 23.28, + "y": 181.18, + "z": 4.5 + }, + "volume": 20.0 + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" } ], "config": { @@ -6129,7 +6162,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "PartialTipMovementNotAllowedError [line 25]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", + "detail": "PartialTipMovementNotAllowedError [line 28]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Moving to NEST 96 Well Plate 200 µL Flat in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -6138,7 +6171,7 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", + "detail": "Moving to NEST 96 Well Plate 200 µL Flat in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", "errorCode": "2004", "errorInfo": {}, "errorType": "PartialTipMovementNotAllowedError", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json index 32e9e2f9294..cdb9d4235a9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json @@ -3606,6 +3606,39 @@ }, "startedAt": "TIMESTAMP", "status": "succeeded" + }, + { + "commandType": "dispense", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "ddccee6754fe0092b9c66898d66b79a7", + "notes": [], + "params": { + "flowRate": 160.0, + "labwareId": "UUID", + "pipetteId": "UUID", + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -9.8 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 23.28, + "y": 181.18, + "z": 4.5 + }, + "volume": 20.0 + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" } ], "config": { @@ -3619,7 +3652,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "PartialTipMovementNotAllowedError [line 21]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", + "detail": "UnexpectedProtocolError [line 22]: Error 4000 GENERAL_ERROR (UnexpectedProtocolError): Cannot return tip to a tiprack while the pipette is configured for partial tip.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -3628,10 +3661,10 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", - "errorCode": "2004", + "detail": "Cannot return tip to a tiprack while the pipette is configured for partial tip.", + "errorCode": "4000", "errorInfo": {}, - "errorType": "PartialTipMovementNotAllowedError", + "errorType": "UnexpectedProtocolError", "id": "UUID", "isDefined": false, "wrappedErrors": [] diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json index de6ff1e98f5..1e9b318abf5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json @@ -3535,7 +3535,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "APIVersionError [line 16]: Error 4011 INCORRECT_API_VERSION (APIVersionError): Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20. is not yet available in the API version in use.", + "detail": "APIVersionError [line 16]: Error 4011 INCORRECT_API_VERSION (APIVersionError): Nozzle layout configuration of style SINGLE is not available until API version 2.20. You are currently using API version 2.16.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -3544,12 +3544,12 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20. is not yet available in the API version in use.", + "detail": "Nozzle layout configuration of style SINGLE is not available until API version 2.20. You are currently using API version 2.16.", "errorCode": "4011", "errorInfo": { - "current_version": null, - "identifier": "Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20.", - "until_version": null + "current_version": "2.16", + "identifier": "Nozzle layout configuration of style SINGLE", + "until_version": "2.20" }, "errorType": "APIVersionError", "id": "UUID", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json index 5157314f76f..04d54b06b4e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json @@ -1349,7 +1349,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "PartialTipMovementNotAllowedError [line 20]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Moving to Opentrons Flex 96 Tip Rack 200 µL in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", + "detail": "PartialTipMovementNotAllowedError [line 20]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -1358,7 +1358,7 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "Moving to Opentrons Flex 96 Tip Rack 200 µL in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", + "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", "errorCode": "2004", "errorInfo": {}, "errorType": "PartialTipMovementNotAllowedError", diff --git a/api-client/Makefile b/api-client/Makefile index 1ac1ecbc08d..3eb4fd7f05e 100644 --- a/api-client/Makefile +++ b/api-client/Makefile @@ -4,6 +4,12 @@ # TODO(mc, 2021-02-12): this may be unnecessary by using `yarn run` instead SHELL := bash +# These variables can be overriden when make is invoked to customize the +# behavior of jest +tests ?= +cov_opts ?= --coverage=true +test_opts ?= + # standard targets ##################################################################### @@ -20,4 +26,8 @@ build: .PHONY: test test: - $(MAKE) -C .. test-js-api-client + $(MAKE) -C .. test-js-api-client tests="$(tests)" test_opts="$(test_opts)" + +.PHONY: test-cov +test-cov: + make -C .. test-js-api-client tests=$(tests) test_opts="$(test_opts)" cov_opts="$(cov_opts)" diff --git a/api-client/src/runs/commands/getCommands.ts b/api-client/src/runs/commands/getCommands.ts index 4833b94e5a8..95283c81b64 100644 --- a/api-client/src/runs/commands/getCommands.ts +++ b/api-client/src/runs/commands/getCommands.ts @@ -3,12 +3,12 @@ import { GET, request } from '../../request' import type { ResponsePromise } from '../../request' import type { HostConfig } from '../../types' import type { CommandsData } from '..' -import type { GetCommandsParams } from './types' +import type { GetRunCommandsParamsRequest } from './types' export function getCommands( config: HostConfig, runId: string, - params: GetCommandsParams + params: GetRunCommandsParamsRequest ): ResponsePromise { return request( GET, diff --git a/api-client/src/runs/commands/getCommandsAsPreSerializedList.ts b/api-client/src/runs/commands/getCommandsAsPreSerializedList.ts index 420f984b280..1d96f3d2209 100644 --- a/api-client/src/runs/commands/getCommandsAsPreSerializedList.ts +++ b/api-client/src/runs/commands/getCommandsAsPreSerializedList.ts @@ -4,13 +4,13 @@ import type { ResponsePromise } from '../../request' import type { HostConfig } from '../../types' import type { CommandsAsPreSerializedListData, - GetCommandsParams, + GetRunCommandsParamsRequest, } from './types' export function getCommandsAsPreSerializedList( config: HostConfig, runId: string, - params: GetCommandsParams + params: GetRunCommandsParamsRequest ): ResponsePromise { return request( GET, diff --git a/api-client/src/runs/commands/types.ts b/api-client/src/runs/commands/types.ts index cd18924201c..8c5517a1fe3 100644 --- a/api-client/src/runs/commands/types.ts +++ b/api-client/src/runs/commands/types.ts @@ -5,6 +5,14 @@ export interface GetCommandsParams { pageLength: number // the number of items to include } +export interface GetRunCommandsParams extends GetCommandsParams { + includeFixitCommands?: boolean +} + +export interface GetRunCommandsParamsRequest extends GetCommandsParams { + includeFixitCommands: boolean | null +} + export interface RunCommandErrors { data: RunCommandError[] meta: GetCommandsParams & { totalLength: number } diff --git a/api/docs/static/override_sphinx.css b/api/docs/static/override_sphinx.css index 20e923f16c4..10a1d091b94 100644 --- a/api/docs/static/override_sphinx.css +++ b/api/docs/static/override_sphinx.css @@ -1,11 +1,12 @@ -@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700'); +@import url('https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,600,700'); +@import url('https://fonts.googleapis.com/css2?family=Reddit+Mono:wght@200..900&display=swap'); /* OT NAV */ body { padding: 0; margin: 0; - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; } .highlight-none, .mi, .literal { @@ -35,7 +36,18 @@ div.document [id] { div.body p { line-height: 20pt; - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; +} + +pre, tt, code { + font-size: 0.9em; + font-family: "Reddit Mono", "Consolas", "Lucida Console", monospace; +} + +/* classes for API Reference docstring signatures */ +.sig, .sig-name, code.descname, .sig-prename, .optional, .sig-paren { + font-size: 1em; + font-family: "Reddit Mono", "Consolas", "Lucida Console", monospace; } div.body h1 { @@ -90,7 +102,13 @@ div.body h2, div.body h3, div.body h4, div.body h5 { - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; +} + +/* Links need an extra two pixels of padding to compensate between body font height +being 1em and code font height being 0.9em */ +a.reference { + padding-bottom: 2px; } /* Suppressing the display of the toctrees rendered in the doc body means we @@ -193,13 +211,13 @@ div.body p.caption { ul { /* margin-left: 0; */ - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; } ul ul { list-style-type: circle; margin-left: 30px; - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; } @media screen and (min-device-width: 320px)and (max-device-width: 640px) { diff --git a/api/docs/v2/modules/temperature_module.rst b/api/docs/v2/modules/temperature_module.rst index 5debe628a95..845f6c69931 100644 --- a/api/docs/v2/modules/temperature_module.rst +++ b/api/docs/v2/modules/temperature_module.rst @@ -39,6 +39,8 @@ You can use these standalone adapter definitions to load Opentrons verified or c - ``opentrons_aluminum_flat_bottom_plate`` * - Opentrons 96 Well Aluminum Block - ``opentrons_96_well_aluminum_block`` + * - Opentrons 96 Deep Well Temperature Module Adapter + - ``opentrons_96_deep_well_temp_mod_adapter`` For example, these commands load a PCR plate on top of the 96-well block:: diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index 0fd8deb4afb..a71ad5cf4a2 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -91,6 +91,9 @@ Useful Types .. automodule:: opentrons.types :members: PipetteNotAttachedError, Point, Location, Mount +.. autoclass:: opentrons.protocol_api.CSVParameter + :members: + .. autodata:: opentrons.protocol_api.OFF_DECK :no-value: diff --git a/api/docs/v2/parameters/defining.rst b/api/docs/v2/parameters/defining.rst index 6b596ec8a0a..eb8caf4c957 100644 --- a/api/docs/v2/parameters/defining.rst +++ b/api/docs/v2/parameters/defining.rst @@ -28,7 +28,7 @@ Depending on the :ref:`type of parameter `, you'll need to specify so - An optional longer explanation of what the parameter does, or how its values will affect the execution of the protocol. - Maximum 100 characters. * - ``default`` - - + - - The value the parameter will have if the technician makes no changes to it during run setup. * - ``minimum`` and ``maximum`` - @@ -71,7 +71,9 @@ Within this function definition, call methods on ``parameters`` to define parame Types of Parameters =================== -The API supports four types of parameters: Boolean (:py:class:`bool`), integer (:py:class:`int`), floating point number (:py:class:`float`), and string (:py:class:`str`). It is not possible to mix types within a single parameter. +The API supports four types of parameters that correspond to Python built-in types: Boolean (:py:class:`bool`), integer (:py:class:`int`), floating point number (:py:class:`float`), and string (:py:class:`str`). It is not possible to mix types within a single parameter. + +In addition, starting in version 2.20, the API supports CSV files as parameters. All data contained in CSV parameters, including numeric data, is initially interpreted as strings. See :ref:`rtp-csv-data` for more information. Boolean Parameters ------------------ @@ -179,3 +181,25 @@ A common use for string display names is to provide an easy-to-read version of a During run setup, the technician can choose from a menu of the provided choices. .. versionadded:: 2.18 + +CSV Parameters +-------------- + +CSV parameters accept any valid comma-separated file. You don't need to specify the format of the data. Due to this flexibility, CSV parameters do not have default values. Separately provide standard operating procedures or template files to the scientists and technicians who will create the tabular data your protocol relies on. + +Briefly describe the purpose of your CSV parameter when defining it. + +.. code-block:: + + parameters.add_csv_file( + variable_name="cherrypicking_wells", + display_name="Cherrypicking wells", + description="Table of labware, wells, and volumes to transfer." + ) + +During run setup, the technician can use the Flex touchscreen to choose a CSV file. They can choose from files on an attached USB drive, or from files already associated with the protocol and stored on the robot. Or in the Opentrons App, they can choose any file on their computer. + +.. note:: + The touchscreen and app currently limit you to selecting one CSV file per run. To match this limitation, the API will raise an error if you define more than one CSV parameter. + +.. versionadded:: 2.20 diff --git a/api/docs/v2/parameters/use_case_cherrypicking.rst b/api/docs/v2/parameters/use_case_cherrypicking.rst new file mode 100644 index 00000000000..e721aa75816 --- /dev/null +++ b/api/docs/v2/parameters/use_case_cherrypicking.rst @@ -0,0 +1,177 @@ +:og:description: How to use a CSV parameter to perform cherrypicking in an Opentrons Python protocol. + +.. _use-case-cherrypicking: + +********************************** +Parameter Use Case – Cherrypicking +********************************** + +A common liquid handling task is `cherrypicking`: pipetting liquid from only certain wells on a source plate and placing them in order on a destination plate. This use case demonstrates how to use a CSV runtime parameter to automate this process and to customize it on every run — without having to modify the Python protocol itself. + +In this simple example, the CSV will only control: + + - Source slot + - Source well + - Volume to transfer + +The destination labware and well order will remain fixed, to focus on using these three pieces of data with the :py:meth:`.transfer` function. In actual use, you can further customize pipetting behavior by adding more runtime parameters or by adding columns to your CSV file. + +Preparing the CSV +================= + +To get started, let's set up the CSV parameter. The data format we expect for this protocol is simple enough to fully explain in the parameter's description. + +.. code-block:: python + + def add_parameters(parameters): + + parameters.add_csv_file( + variable_name="cherrypicking_wells", + display_name="Cherrypicking wells", + description=( + "Table with three columns:" + " source slot, source well," + " and volume to transfer in µL." + ) + ) + +Here is an example of a CSV file that fits this format, specifying three wells across two plates: + +.. code-block:: text + + source slot,source well,volume + D1,A1,50 + D1,C4,30 + D2,H1,50 + +The protocol will rely on the data being structured exactly this way, with a header row and the three columns in this order. The technician would select this, or another file with the same structure, during run setup. + +Our protocol will use the information contained in the selected CSV for loading labware in the protocol and performing the cherrypicking transfers. + +Parsing the CSV +=============== + +We'll use the Python API's :py:meth:`.parse_as_csv` method to allow easy access to different portions of the CSV data at different points in the protocol:: + + def run(protocol): + + well_data = protocol.params.cherrypicking_wells.parse_as_csv() + +Now ``well_data`` is a list with four elements, one for each row in the file. We'll use the rows in a ``for`` loop later in the protocol, when it's time to transfer liquid. + +Loading Source Labware +====================== + +We'll use the data from the ``source slot`` column as part of loading the source labware. Let's assume that we always use Opentrons Tough PCR plates for both source and destination plates. Then we need to determine the locations for loading source plates from the first column of the CSV. This will have three steps: + + - Using a list comprehension to get data from the ``source slot`` column. + - Deduplicating the items in the column. + - Looping over the unique items to load the plates. + +First, we'll get all of the data from the first column of the CSV, using a list comprehension. Then we'll take a slice of the resulting list to remove the header:: + + source_slots = [row[0] for row in well_data][1:] + # ['D1', 'D1', 'D2'] + +Next, we'll get the unique items in the list by converting it to a :py:obj:`set` and back to a list:: + + unique_source_slots = list(set(source_slots)) + # ['D1', 'D2'] + +Finally, we'll loop over those slot names to load labware:: + + for slot in unique_source_slots:: + protocol.load_labware( + load_name="opentrons_96_wellplate_200ul_pcr_full_skirt", + location=slot + ) + +Note that loading labware in a loop like this doesn't assign each labware instance to a variable. That's fine, because we'll use :py:obj:`.ProtocolContext.deck` to refer to them by slot name later on. + +The entire start of the ``run()`` function, including a pipette and fixed labware (i.e., labware not affected by the CSV runtime parameter) will look like this: + +.. code-block:: python + :substitutions: + + from opentrons import protocol_api + + requirements = {"robotType": "Flex", "apiLevel": "|apiLevel|"} + + def add_parameters(parameters): + + parameters.add_csv_file( + variable_name="cherrypicking_wells", + display_name="Cherrypicking wells", + description=( + "Table with three columns:" + " source slot, source well," + " and volume to transfer in µL." + ) + ) + + def run(protocol: protocol_api.ProtocolContext): + well_data = protocol.params.cherrypicking_wells.parse_as_csv() + source_slots = [row[0] for row in well_data][1::] + unique_source_slots = list(set(source_slots)) + + # load tip rack in deck slot C1 + tiprack = protocol.load_labware( + load_name="opentrons_flex_96_tiprack_1000ul", location="C1" + ) + # attach pipette to left mount + pipette = protocol.load_instrument( + instrument_name="flex_1channel_1000", + mount="left", + tip_racks=[tiprack] + ) + # load trash bin + trash = protocol.load_trash_bin("A3") + # load destination plate in deck slot C2 + dest_plate = protocol.load_labware( + load_name="opentrons_96_wellplate_200ul_pcr_full_skirt", + location="C2" + ) + # load source plates based on CSV data + for slot in unique_source_slots: + protocol.load_labware( + load_name="opentrons_96_wellplate_200ul_pcr_full_skirt", + location=slot + ) + +Picking the Cherries +==================== + +Now it's time to transfer liquid based on the data in each row of the CSV. + +Once again we'll start by slicing off the header row of ``well_data``. Each remaining row has the source slot, source well, and volume data that we can directly pass to :py:meth:`.transfer`. + +We also need to specify the destination well. We want the destinations to proceed in order according to :py:meth:`.Labware.wells`. To track this all in a single loop, we'll wrap our CSV data in an :py:obj:`.enumerate` object to provide an index that increments each time through the loop. All together, the transfer loop looks like this:: + + for index, row in enumerate(well_data[1::]): + # get source location from CSV + source_slot = row[0] + source_well = row[1] + source_location = protocol.deck[source_slot][source_well] + + # get volume as a number + transfer_volume = float(row[2]) + + # get destination location from loop index + dest_location = dest_plate.wells()[index] + + # perform parameterized transfer + pipette.transfer( + volume=transfer_volume, + source=source_location, + dest=dest_location + ) + +Let's unpack this. For each time through the loop, we build the source location from the first (``row[0]``) and second (``row[1]``) item in the row list. We then construct a complete location with respect to ``protocol.deck``. + +Next, we get the volume for the transfer. All CSV data is treated as strings, so we have to cast it to a floating point number. + +The last piece of information needed is the destination well. We take the index of the current iteration through the loop, and use that same index with respect to the ordered list of all wells on the destination plate. + +With all the information gathered and stored in variables, all that's left is to pass that information as the arguments of ``transfer()``. With our example file, this will execute three transfers. By using a different CSV at run time, this code could complete up to 96 transfers (at which point it would run out of both tips and destination wells). + +For more complex transfer behavior — such as adjusting location within the well — you could extend the CSV format and the associated code to work with additional data. And check out the `verified cherrypicking protocol `_ in the Opentrons Protocol Library for further automation based on CSV data, including loading different types of plates, automatically loading tip racks, and more. diff --git a/api/docs/v2/parameters/using_values.rst b/api/docs/v2/parameters/using_values.rst index 754300347c9..c3265ae1ecd 100644 --- a/api/docs/v2/parameters/using_values.rst +++ b/api/docs/v2/parameters/using_values.rst @@ -28,10 +28,12 @@ Then ``params`` will gain three attributes: ``params.dry_run``, ``params.sample_ You can also save parameter values to variables with names of your choosing. +.. _using-rtp-types: + Parameter Types =============== -Each attribute of ``params`` has the type corresponding to its parameter definition. Keep in mind the parameter's type when using its value in different contexts. +Each attribute of ``params`` has the type corresponding to its parameter definition (except CSV parameters; see :ref:`rtp-csv-data` below). Keep in mind the parameter's type when using its value in different contexts. Say you wanted to add a comment to the run log, stating how many samples the protocol will process. Since ``sample_count`` is an ``int``, you'll need to cast it to a ``str`` or the API will raise an error. @@ -43,6 +45,44 @@ Say you wanted to add a comment to the run log, stating how many samples the pro Also be careful with ``int`` types when performing calculations: dividing an ``int`` by an ``int`` with the ``/`` operator always produces a ``float``, even if there is no remainder. The :ref:`sample count use case ` converts a sample count to a column count by dividing by 8 — but it uses the ``//`` integer division operator, so the result can be used for creating ranges, slicing lists, and as ``int`` argument values without having to cast it in those contexts. +.. _rtp-csv-data: + +Manipulating CSV Data +===================== + +CSV parameters have their own :py:class:`.CSVParameter` type, since they don't correspond to a built-in Python type. This class has properties and methods that let you access the CSV data in one of three ways: as a file handler, as a string, or as nested lists. + +The :py:obj:`.CSVParameter.file` parameter provides a read-only `file handler object `_ that points to your CSV data. You can pass this object to functions of the built-in :py:obj:`csv` module, or to other modules you import, such as ``pandas``. + +The :py:obj:`.CSVParameter.contents` parameter returns the entire contents of the CSV file as a single string. You then need to parse the data yourself to extract the information you need. + +The :py:meth:`.CSVParameter.parse_as_csv` method returns CSV data in a structured format. Specifically, it is a list of lists of strings. This lets you access any "cell" of your tabular data by row and column index. This example parses a runtime parameter named ``csv_data``, stores the parsed data as ``parsed_csv``, and then accesses different portions of the data:: + + parsed_csv = protocol.params.csv_data.parse_as_csv() + parsed_csv[0] # first row (header, if present) + parsed_csv[1][2] # second row, third column + [row[1] for row in parsed_csv] # second column + +.. versionadded:: 2.20 + +Like all Python lists, the lists representing your CSVs are zero-indexed. + +.. tip:: + + CSV parameters don't have default values. Accessing CSV data in any of the above ways will prevent protocol analysis from completing until you select a CSV file and confirm all runtime parameter values during run setup. + + You can use a try–except block to work around this and provide the data needed for protocol analysis. First, add ``from opentrons.protocol_api import RuntimeParameterRequiredError`` at the top of your protocol. Then catch the error like this:: + + try: + parsed_csv = protocol.params.csv_data.parse_as_csv() + except RuntimeParameterRequiredError: + parsed_csv = [ + ["source slot", "source well", "volume"], + ["D1", "A1", "50"], + ["D2", "B1", "50"], + ] + + Limitations =========== diff --git a/api/docs/v2/runtime_parameters.rst b/api/docs/v2/runtime_parameters.rst index 71689eedb50..810902eb729 100644 --- a/api/docs/v2/runtime_parameters.rst +++ b/api/docs/v2/runtime_parameters.rst @@ -12,6 +12,7 @@ Runtime Parameters parameters/using_values parameters/use_case_sample_count parameters/use_case_dry_run + parameters/use_case_cherrypicking parameters/style Runtime parameters let you define user-customizable variables in your Python protocols. This gives you greater flexibility and puts extra control in the hands of the technician running the protocol — without forcing them to switch between lots of protocol files or write code themselves. @@ -26,4 +27,5 @@ It continues with a selection of use cases and some overall style guidance. When - :ref:`Use case – sample count `: Change behavior throughout a protocol based on how many samples you plan to process. Setting sample count exactly saves time, tips, and reagents. - :ref:`Use case – dry run `: Test your protocol, rather than perform a live run, just by flipping a toggle. +- :ref:`Use case – cherrypicking `: Use a CSV file to specify locations and volumes for a simple cherrypicking protocol. - :ref:`Style and usage `: When you're a protocol author, you write code. When you're a parameter author, you write words. Follow this advice to make things as clear as possible for the technicians who will run your protocol. diff --git a/api/release-notes.md b/api/release-notes.md index b855d1d10a1..8664eac3f8d 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -38,6 +38,7 @@ Welcome to the v7.5.0 release of the Opentrons robot software! ### Bug Fixes - Fixed certain string runtime parameter values being misinterpreted as an incorrect type. +- The `opentrons_execute` command-line tool and `opentrons.execute` Python API functions now take the deck configuration of Flex into account when planning gantry movement. ### Known Issue diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 8e90e08190b..0b08b20e17e 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -275,6 +275,7 @@ async def _do_analyze(protocol_source: ProtocolSource) -> RunResult: modules=[], labwareOffsets=[], liquids=[], + wells=[], hasEverEnteredErrorRecovery=False, ), parameters=[], diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index 6f5a7362520..08b86f16c95 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -24,12 +24,13 @@ DEFAULT_LIQUID_PROBE_SETTINGS: Final[LiquidProbeSettings] = LiquidProbeSettings( mount_speed=5, - plunger_speed=20, + plunger_speed=15, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.sync_buffer_to_csv, aspirate_while_sensing=False, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files={InstrumentProbeType.PRIMARY: "/data/pressure_sensor_data.csv"}, @@ -349,6 +350,9 @@ def _build_default_liquid_probe( z_overlap_between_passes_mm=from_conf.get( "z_overlap_between_passes_mm", default.z_overlap_between_passes_mm ), + plunger_reset_offset=from_conf.get( + "plunger_reset_offset", default.plunger_reset_offset + ), samples_for_baselining=from_conf.get( "samples_for_baselining", default.samples_for_baselining ), diff --git a/api/src/opentrons/config/types.py b/api/src/opentrons/config/types.py index 339fbb3c8a1..5a6c67725d0 100644 --- a/api/src/opentrons/config/types.py +++ b/api/src/opentrons/config/types.py @@ -138,6 +138,7 @@ class LiquidProbeSettings: output_option: OutputOptions aspirate_while_sensing: bool z_overlap_between_passes_mm: float + plunger_reset_offset: float samples_for_baselining: int sample_time_sec: float data_files: Optional[Dict[InstrumentProbeType, str]] diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 71ce9833251..dfa6fd99ce4 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -147,6 +147,7 @@ async def liquid_probe( plunger_speed: float, threshold_pascals: float, plunger_impulse_time: float, + num_baseline_reads: int, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 386e6a36159..ced7540d4df 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1358,6 +1358,7 @@ async def liquid_probe( plunger_speed: float, threshold_pascals: float, plunger_impulse_time: float, + num_baseline_reads: int, output_option: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, @@ -1389,6 +1390,7 @@ async def liquid_probe( mount_speed=mount_speed, threshold_pascals=threshold_pascals, plunger_impulse_time=plunger_impulse_time, + num_baseline_reads=num_baseline_reads, csv_output=csv_output, sync_buffer_output=sync_buffer_output, can_bus_only_output=can_bus_only_output, diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 97d3661e32e..034531892d8 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -346,6 +346,7 @@ async def liquid_probe( plunger_speed: float, threshold_pascals: float, plunger_impulse_time: float, + num_baseline_reads: int, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 3196645eee2..bea325cfb83 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2636,6 +2636,7 @@ async def _liquid_probe_pass( (probe_settings.plunger_speed * plunger_direction), probe_settings.sensor_threshold_pascals, probe_settings.plunger_impulse_time, + probe_settings.samples_for_baselining, probe_settings.output_option, probe_settings.data_files, probe=probe, @@ -2686,7 +2687,6 @@ async def liquid_probe( probe_settings = deepcopy(self.config.liquid_sense) # We need to significatly slow down the 96 channel liquid probe - # TODO: (sigler) add LLD plunger-speed to pipette definitions if self.gantry_load == GantryLoad.HIGH_THROUGHPUT: max_plunger_speed = self.config.motion_settings.max_speed_discontinuity[ GantryLoad.HIGH_THROUGHPUT @@ -2718,8 +2718,9 @@ async def liquid_probe( # height that is considered safe to reset the plunger without disturbing liquid # this usually needs to at least 1-2mm from liquid, to avoid splashes from air - # TODO: (sigler) add this to pipette's liquid def (per tip) - z_offset_for_plunger_prep = max(2.0, z_offset_per_pass) + z_offset_for_plunger_prep = max( + probe_settings.plunger_reset_offset, z_offset_per_pass + ) async def prep_plunger_for_probe_move( position: top_types.Point, aspirate_while_sensing: bool @@ -2733,7 +2734,6 @@ async def prep_plunger_for_probe_move( # Prep the plunger await self.move_to(checked_mount, mount_pos_for_plunger_prep) if aspirate_while_sensing: - # TODO(cm, 7/8/24): remove p_prep_speed from the rate at some point await self._move_to_plunger_bottom(checked_mount, rate=1) else: await self._move_to_plunger_top_for_liquid_probe(checked_mount, rate=1) diff --git a/api/src/opentrons/hardware_control/protocols/motion_controller.py b/api/src/opentrons/hardware_control/protocols/motion_controller.py index 8387e4a907c..daaf166f283 100644 --- a/api/src/opentrons/hardware_control/protocols/motion_controller.py +++ b/api/src/opentrons/hardware_control/protocols/motion_controller.py @@ -204,6 +204,10 @@ async def disengage_axes(self, which: List[Axis]) -> None: """Disengage some axes.""" ... + async def engage_axes(self, which: List[Axis]) -> None: + """Engage some axes.""" + ... + async def retract(self, mount: MountArgType, margin: float = 10) -> None: """Pull the specified mount up to its home position. diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 405aa2256a7..6ebb47f0ac8 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -416,23 +416,48 @@ def _is_within_pipette_extents( pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point], ) -> bool: """Whether a given point is within the extents of a configured pipette on the specified robot.""" - mount = engine_state.pipettes.get_mount(pipette_id) - robot_extent_per_mount = engine_state.geometry.absolute_deck_extents - pip_back_left_bound, pip_front_right_bound, _, _ = pipette_bounding_box_at_loc - pipette_bounds_offsets = engine_state.pipettes.get_pipette_bounding_box(pipette_id) - from_back_right = ( - robot_extent_per_mount.back_right[mount] - + pipette_bounds_offsets.back_right_corner - ) - from_front_left = ( - robot_extent_per_mount.front_left[mount] - + pipette_bounds_offsets.front_left_corner - ) + channels = engine_state.pipettes.get_channels(pipette_id) + robot_extents = engine_state.geometry.absolute_deck_extents + ( + pip_back_left_bound, + pip_front_right_bound, + pip_back_right_bound, + pip_front_left_bound, + ) = pipette_bounding_box_at_loc + + # Given the padding values accounted for against the deck extents, + # a pipette is within extents when all of the following are true: + + # Each corner slot full pickup case: + # A1: Front right nozzle is within the rear and left-side padding limits + # D1: Back right nozzle is within the front and left-side padding limits + # A3 Front left nozzle is within the rear and right-side padding limits + # D3: Back left nozzle is within the front and right-side padding limits + # Thermocycler Column A2: Front right nozzle is within padding limits + + if channels == 96: + return ( + pip_front_right_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_front_right_bound.x >= robot_extents.padding_left_side + and pip_back_right_bound.y >= robot_extents.padding_front + and pip_back_right_bound.x >= robot_extents.padding_left_side + and pip_front_left_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_front_left_bound.x + <= robot_extents.deck_extents.x + robot_extents.padding_right_side + and pip_back_left_bound.y >= robot_extents.padding_front + and pip_back_left_bound.x + <= robot_extents.deck_extents.x + robot_extents.padding_right_side + ) + # For 8ch pipettes we only check the rear and front extents return ( - from_back_right.x >= pip_back_left_bound.x >= from_front_left.x - and from_back_right.y >= pip_back_left_bound.y >= from_front_left.y - and from_back_right.x >= pip_front_right_bound.x >= from_front_left.x - and from_back_right.y >= pip_front_right_bound.y >= from_front_left.y + pip_front_right_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_back_right_bound.y >= robot_extents.padding_front + and pip_front_left_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_back_left_bound.y >= robot_extents.padding_front ) diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index 6743a8a39c5..ec7307a6a90 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -125,6 +125,17 @@ def get_center(self) -> Point: well_location=WellLocation(origin=WellOrigin.CENTER), ) + def get_meniscus(self, z_offset: float) -> Point: + """Get the coordinate of the well's meniscus, with a z-offset.""" + return self._engine_client.state.geometry.get_well_position( + well_name=self._name, + labware_id=self._labware_id, + well_location=WellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=0, y=0, z=z_offset), + ), + ) + def load_liquid( self, liquid: Liquid, diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py index a88dd2eee80..f37aefbd4be 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py @@ -106,6 +106,10 @@ def get_center(self) -> Point: """Get the coordinate of the well's center.""" return self._geometry.center() + def get_meniscus(self, z_offset: float) -> Point: + """This will never be called because it was added in API 2.21.""" + assert False, "get_meniscus only supported in API 2.21 & later" + def load_liquid( self, liquid: Liquid, diff --git a/api/src/opentrons/protocol_api/core/well.py b/api/src/opentrons/protocol_api/core/well.py index bd58963a59c..81dddede2f1 100644 --- a/api/src/opentrons/protocol_api/core/well.py +++ b/api/src/opentrons/protocol_api/core/well.py @@ -71,6 +71,10 @@ def get_bottom(self, z_offset: float) -> Point: def get_center(self) -> Point: """Get the coordinate of the well's center.""" + @abstractmethod + def get_meniscus(self, z_offset: float) -> Point: + """Get the coordinate of the well's meniscus, with an z-offset.""" + @abstractmethod def load_liquid( self, diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 60c2f5a705c..7121567c3c4 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -263,7 +263,6 @@ def aspirate( and self._96_tip_config_valid() ): self.require_liquid_presence(well=well) - self.prepare_to_aspirate() with publisher.publish_context( broker=self.broker, @@ -2059,6 +2058,8 @@ def configure_nozzle_layout( NozzleLayout.QUADRANT, ] if style in disabled_layouts: + # todo(mm, 2024-08-20): UnsupportedAPIError boils down to an API_REMOVED + # error code, which is not correct here. raise UnsupportedAPIError( message=f"Nozzle layout configuration of style {style.value} is currently unsupported." ) @@ -2069,7 +2070,11 @@ def configure_nozzle_layout( < _PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN ) and (style not in original_enabled_layouts): raise APIVersionError( - f"Nozzle layout configuration of style {style.value} is unsupported in API Versions lower than {_PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN}." + api_element=f"Nozzle layout configuration of style {style.value}", + until_version=str( + _PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN + ), + current_version=str(self._api_version), ) front_right_resolved = front_right diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 3ad328258c6..43c2c0ce5a8 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -30,7 +30,6 @@ # remove when their usage is no longer needed from opentrons.protocols.labware import ( # noqa: F401 get_labware_definition as get_labware_definition, - get_all_labware_definitions as get_all_labware_definitions, verify_definition as verify_definition, save_definition as save_definition, ) @@ -222,6 +221,17 @@ def center(self) -> Location: """ return Location(self._core.get_center(), self) + @requires_version(2, 21) + def meniscus(self, z: float = 0.0) -> Location: + """ + :param z: An offset on the z-axis, in mm. Positive offsets are higher and + negative offsets are lower. + :return: A :py:class:`~opentrons.types.Location` corresponding to the + absolute position of the meniscus-center of the well, plus the ``z`` offset + (if specified). + """ + return Location(self._core.get_meniscus(z_offset=z), self) + @requires_version(2, 8) def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """ @@ -581,7 +591,7 @@ def set_calibration(self, delta: Point) -> None: api_element="Labware.set_calibration()", since_version=f"{ENGINE_CORE_API_VERSION}", current_version=f"{self._api_version}", - message=" Try using the Opentrons App's Labware Position Check.", + extra_message="Try using the Opentrons App's Labware Position Check.", ) self._core.set_calibration(delta) @@ -632,7 +642,7 @@ def set_offset(self, x: float, y: float, z: float) -> None: api_element="Labware.set_offset()", until_version=f"{SET_OFFSET_RESTORED_API_VERSION}", current_version=f"{self._api_version}", - message=" This feature not available in versions 2.14 thorugh 2.17. You can also use the Opentrons App's Labware Position Check.", + extra_message="This feature not available in versions 2.14 thorugh 2.17. You can also use the Opentrons App's Labware Position Check.", ) else: self._core.set_calibration(Point(x=x, y=y, z=z)) @@ -974,7 +984,7 @@ def use_tips(self, start_well: Well, num_channels: int = 1) -> None: api_element="Labware.use_tips", since_version=f"{ENGINE_CORE_API_VERSION}", current_version=f"{self._api_version}", - message=" To modify tip state, use Labware.reset.", + extra_message="To modify tip state, use Labware.reset.", ) assert num_channels > 0, "Bad call to use_tips: num_channels<=0" @@ -1064,7 +1074,7 @@ def return_tips(self, start_well: Well, num_channels: int = 1) -> None: api_element="Labware.return_tips()", since_version=f"{ENGINE_CORE_API_VERSION}", current_version=f"{self._api_version}", - message=" Use Labware.reset() instead.", + extra_message="Use Labware.reset() instead.", ) # This logic is the inverse of :py:meth:`use_tips` diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index 020170389d2..b2abeca24e4 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -103,7 +103,7 @@ def load_labware_object(self, labware: Labware) -> Labware: raise UnsupportedAPIError( api_element="`ModuleContext.load_labware_object`", since_version="2.14", - message=" Use `ModuleContext.load_labware` or `load_labware_by_definition` instead.", + extra_message="Use `ModuleContext.load_labware` or `load_labware_by_definition` instead.", ) _log.warning( @@ -305,7 +305,7 @@ def geometry(self) -> LegacyModuleGeometry: raise UnsupportedAPIError( api_element="`ModuleContext.geometry`", since_version="2.14", - message=" Use properties of the `ModuleContext` itself.", + extra_message="Use properties of the `ModuleContext` itself.", ) def __repr__(self) -> str: @@ -482,7 +482,7 @@ def engage( api_element="The height parameter of MagneticModuleContext.engage()", since_version=f"{_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN}", current_version=f"{self._api_version}", - message=" Use offset or height_from_base.", + extra_message="Use offset or height_from_base.", ) self._core.engage(height_from_home=height) diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index bc5b553acf1..0fca3fdc8f3 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -283,7 +283,7 @@ def max_speeds(self) -> AxisMaxSpeeds: api_element="ProtocolContext.max_speeds", since_version=f"{ENGINE_CORE_API_VERSION}", current_version=f"{self._api_version}", - message=" Set speeds using InstrumentContext.default_speed or the per-method 'speed' argument.", + extra_message="Set speeds using InstrumentContext.default_speed or the per-method 'speed' argument.", ) return self._core.get_max_speeds() @@ -932,6 +932,7 @@ def load_instrument( from the Opentrons App or touchscreen. :param bool liquid_presence_detection: If ``True``, enable automatic :ref:`liquid presence detection ` for Flex 1-, 8-, or 96-channel pipettes. + .. versionadded:: 2.20 """ instrument_name = validation.ensure_lowercase_name(instrument_name) @@ -1065,7 +1066,7 @@ def resume(self) -> None: api_element="A Python Protocol safely resuming itself after a pause", since_version=f"{ENGINE_CORE_API_VERSION}", current_version=f"{self._api_version}", - message=" To wait automatically for a period of time, use ProtocolContext.delay().", + extra_message="To wait automatically for a period of time, use ProtocolContext.delay().", ) # TODO(mc, 2023-02-13): this assert should be enough for mypy @@ -1186,7 +1187,7 @@ def fixed_trash(self) -> Union[Labware, TrashBin]: api_element="Fixed Trash", since_version="2.16", current_version=f"{self._api_version}", - message=" Fixed trash is no longer supported on Flex protocols.", + extra_message="Fixed trash is no longer supported on Flex protocols.", ) disposal_locations = self._core.get_disposal_locations() if len(disposal_locations) == 0: @@ -1259,7 +1260,7 @@ def define_liquid( api_element="Calling `define_liquid()` without a `description`", current_version=str(self._api_version), until_version=str(desc_and_display_color_omittable_since), - message="Use a newer API version or explicitly supply `description=None`.", + extra_message="Use a newer API version or explicitly supply `description=None`.", ) else: description = None @@ -1269,7 +1270,7 @@ def define_liquid( api_element="Calling `define_liquid()` without a `display_color`", current_version=str(self._api_version), until_version=str(desc_and_display_color_omittable_since), - message="Use a newer API version or explicitly supply `display_color=None`.", + extra_message="Use a newer API version or explicitly supply `display_color=None`.", ) else: display_color = None diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 1ad6628ae24..08e56fdef8f 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -208,7 +208,7 @@ def ensure_and_convert_deck_slot( api_element=f"Specifying a deck slot like '{deck_slot}'", until_version=f"{_COORDINATE_DECK_LABEL_VERSION_GATE}", current_version=f"{api_version}", - message=f"Increase your protocol's apiLevel, or use slot '{alternative}' instead.", + extra_message=f"Increase your protocol's apiLevel, or use slot '{alternative}' instead.", ) return parsed_slot.to_equivalent_for_robot_type(robot_type) diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 2538b67daf5..14c5c3f3fc5 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -18,15 +18,10 @@ CommandType, CommandIntent, ) -from .state import ( - State, - StateView, - StateSummary, - CommandSlice, - CommandPointer, - Config, - CommandErrorSlice, -) +from .state.state import State, StateView +from .state.state_summary import StateSummary +from .state.commands import CommandSlice, CommandErrorSlice, CommandPointer +from .state.config import Config from .plugins import AbstractPlugin from .types import ( diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 38cbbe18bb3..4569f7866ef 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -3,7 +3,7 @@ Actions can be passed to the ActionDispatcher, where they will trigger reactions in objects that subscribe to the pipeline, like the StateStore. """ -from dataclasses import dataclass +import dataclasses from datetime import datetime from enum import Enum from typing import List, Optional, Union @@ -22,6 +22,7 @@ ) from ..error_recovery_policy import ErrorRecoveryPolicy, ErrorRecoveryType from ..notes.notes import CommandNote +from ..state.update_types import StateUpdate from ..types import ( LabwareOffsetCreate, ModuleDefinition, @@ -31,7 +32,7 @@ ) -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class PlayAction: """Start or resume processing commands in the engine.""" @@ -50,28 +51,28 @@ class PauseSource(str, Enum): PROTOCOL = "protocol" -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class PauseAction: """Pause processing commands in the engine.""" source: PauseSource -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class StopAction: """Request engine execution to stop soon.""" from_estop: bool = False -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class ResumeFromRecoveryAction: """See `ProtocolEngine.resume_from_recovery()`.""" pass -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class FinishErrorDetails: """Error details for the payload of a FinishAction or HardwareStoppedAction.""" @@ -80,7 +81,7 @@ class FinishErrorDetails: created_at: datetime -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class FinishAction: """Gracefully stop processing commands in the engine.""" @@ -95,7 +96,7 @@ class FinishAction: """The fatal error that caused the run to fail.""" -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class HardwareStoppedAction: """An action dispatched after hardware has been stopped for good, for this engine instance.""" @@ -105,14 +106,14 @@ class HardwareStoppedAction: """The error that happened while doing post-run finish steps (homing and dropping tips).""" -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class DoorChangeAction: """Handle events coming in from hardware control.""" door_state: DoorState -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class QueueCommandAction: """Add a command request to the queue.""" @@ -123,7 +124,7 @@ class QueueCommandAction: failed_command_id: Optional[str] = None -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class RunCommandAction: """Mark a given command as running. @@ -135,7 +136,7 @@ class RunCommandAction: started_at: datetime -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class SucceedCommandAction: """Mark a given command as succeeded. @@ -145,10 +146,19 @@ class SucceedCommandAction: command: Command """The command in its new succeeded state.""" + # todo(mm, 2024-08-26): Remove when no state stores use this anymore. + # https://opentrons.atlassian.net/browse/EXEC-639 private_result: CommandPrivateResult + state_update: StateUpdate = dataclasses.field( + # todo(mm, 2024-08-26): This has a default only to make it easier to transition + # old tests while https://opentrons.atlassian.net/browse/EXEC-639 is in + # progress. Make this mandatory when that's completed. + default_factory=StateUpdate + ) -@dataclass(frozen=True) + +@dataclasses.dataclass(frozen=True) class FailCommandAction: """Mark a given command as failed. @@ -196,7 +206,7 @@ class FailCommandAction: """The command to fail, in its prior `running` state.""" -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class AddLabwareOffsetAction: """Add a labware offset, to apply to subsequent `LoadLabwareCommand`s.""" @@ -205,28 +215,28 @@ class AddLabwareOffsetAction: request: LabwareOffsetCreate -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class AddLabwareDefinitionAction: """Add a labware definition, to apply to subsequent `LoadLabwareCommand`s.""" definition: LabwareDefinition -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class AddLiquidAction: """Add a liquid, to apply to subsequent `LoadLiquid`s.""" liquid: Liquid -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class SetDeckConfigurationAction: """See `ProtocolEngine.set_deck_configuration()`.""" deck_configuration: Optional[DeckConfigurationType] -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class AddAddressableAreaAction: """Add a single addressable area to state. @@ -238,7 +248,7 @@ class AddAddressableAreaAction: addressable_area: AddressableAreaLocation -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class AddModuleAction: """Add an attached module directly to state without a location.""" @@ -248,14 +258,14 @@ class AddModuleAction: module_live_data: LiveData -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class ResetTipsAction: """Reset the tip tracking state of a given tip rack.""" labware_id: str -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class SetPipetteMovementSpeedAction: """Set the speed of a pipette's X/Y/Z movements. Does not affect plunger speed. @@ -266,7 +276,7 @@ class SetPipetteMovementSpeedAction: speed: Optional[float] -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class AddAbsorbanceReaderLidAction: """Add the absorbance reader lid id to the absorbance reader module substate. @@ -277,7 +287,7 @@ class AddAbsorbanceReaderLidAction: lid_id: str -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class SetErrorRecoveryPolicyAction: """See `ProtocolEngine.set_error_recovery_policy()`.""" diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index 26356a76a15..d0c21846d19 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -7,7 +7,7 @@ from .. import commands from ..commands.command_unions import CREATE_TYPES_BY_PARAMS_TYPE -from ..state import StateView +from ..state.state import StateView from ..types import ( Liquid, LabwareOffsetCreate, diff --git a/api/src/opentrons/protocol_engine/clients/transports.py b/api/src/opentrons/protocol_engine/clients/transports.py index 348bbc286c2..5d678026fb2 100644 --- a/api/src/opentrons/protocol_engine/clients/transports.py +++ b/api/src/opentrons/protocol_engine/clients/transports.py @@ -10,7 +10,7 @@ from ..protocol_engine import ProtocolEngine from ..errors import ProtocolCommandFailedError from ..error_recovery_policy import ErrorRecoveryType -from ..state import StateView +from ..state.state import StateView from ..commands import Command, CommandCreate, CommandResult, CommandStatus diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py index 31c51676e7d..4b1135668d0 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py @@ -16,7 +16,7 @@ from opentrons.drivers.types import AbsorbanceReaderLidStatus if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import ( EquipmentHandler, LabwareMovementHandler, diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py index 469732ee9fd..53150280a80 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py index f12a612f649..e6da9edade5 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py @@ -16,7 +16,7 @@ from opentrons.drivers.types import AbsorbanceReaderLidStatus if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import ( EquipmentHandler, LabwareMovementHandler, diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py index a3f11e8d886..f361f819c5e 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py @@ -10,7 +10,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 29daea563bb..ac0e34424e6 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -6,7 +6,6 @@ from .pipetting_common import ( OverpressureError, - OverpressureErrorInternalData, PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin, @@ -25,12 +24,13 @@ from opentrons.hardware_control import HardwareControlAPI +from ..state.update_types import StateUpdate from ..types import WellLocation, WellOrigin, CurrentWell, DeckPoint if TYPE_CHECKING: from ..execution import MovementHandler, PipettingHandler from ..resources import ModelUtils - from ..state import StateView + from ..state.state import StateView from ..notes import CommandNoteAdder @@ -53,7 +53,7 @@ class AspirateResult(BaseLiquidHandlingResult, DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[AspirateResult, None], - DefinedErrorData[OverpressureError, OverpressureErrorInternalData], + DefinedErrorData[OverpressureError], ] @@ -92,6 +92,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: ) current_well = None + state_update = StateUpdate() if not ready_to_aspirate: await self._movement.move_to_well( @@ -118,6 +119,13 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_location=params.wellLocation, current_well=current_well, ) + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + state_update.set_pipette_location( + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, + new_deck_point=deck_point, + ) try: volume_aspirated = await self._pipetting.aspirate_in_place( @@ -140,21 +148,16 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: ], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - private=OverpressureErrorInternalData( - position=DeckPoint.construct( - x=position.x, y=position.y, z=position.z - ) - ), + state_update=state_update, ) else: return SuccessData( public=AspirateResult( volume=volume_aspirated, - position=DeckPoint.construct( - x=position.x, y=position.y, z=position.z - ), + position=deck_point, ), private=None, + state_update=state_update, ) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index 23b11598573..44dc2e93768 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -14,7 +14,6 @@ FlowRateMixin, BaseLiquidHandlingResult, OverpressureError, - OverpressureErrorInternalData, ) from .command import ( AbstractCommandImpl, @@ -25,12 +24,11 @@ ) from ..errors.error_occurrence import ErrorOccurrence from ..errors.exceptions import PipetteNotReadyToAspirateError -from ..types import DeckPoint if TYPE_CHECKING: from ..execution import PipettingHandler, GantryMover from ..resources import ModelUtils - from ..state import StateView + from ..state.state import StateView from ..notes import CommandNoteAdder AspirateInPlaceCommandType = Literal["aspirateInPlace"] @@ -50,7 +48,7 @@ class AspirateInPlaceResult(BaseLiquidHandlingResult): _ExecuteReturn = Union[ SuccessData[AspirateInPlaceResult, None], - DefinedErrorData[OverpressureError, OverpressureErrorInternalData], + DefinedErrorData[OverpressureError], ] @@ -123,13 +121,6 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: } ), ), - private=OverpressureErrorInternalData( - position=DeckPoint( - x=current_position.x, - y=current_position.y, - z=current_position.z, - ), - ), ) else: return SuccessData( diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py index f17b4b44ebc..9954ef07cfa 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out.py @@ -3,6 +3,8 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal + +from ..state.update_types import StateUpdate from ..types import DeckPoint from .pipetting_common import ( PipetteIdMixin, @@ -18,7 +20,7 @@ if TYPE_CHECKING: from ..execution import MovementHandler, PipettingHandler - from ..state import StateView + from ..state.state import StateView BlowOutCommandType = Literal["blowout"] @@ -55,19 +57,30 @@ def __init__( async def execute(self, params: BlowOutParams) -> SuccessData[BlowOutResult, None]: """Move to and blow-out the requested well.""" + state_update = StateUpdate() + x, y, z = await self._movement.move_to_well( pipette_id=params.pipetteId, labware_id=params.labwareId, well_name=params.wellName, well_location=params.wellLocation, ) + deck_point = DeckPoint.construct(x=x, y=y, z=z) + state_update.set_pipette_location( + pipette_id=params.pipetteId, + new_labware_id=params.labwareId, + new_well_name=params.wellName, + new_deck_point=deck_point, + ) await self._pipetting.blow_out_in_place( pipette_id=params.pipetteId, flow_rate=params.flowRate ) return SuccessData( - public=BlowOutResult(position=DeckPoint(x=x, y=y, z=z)), private=None + public=BlowOutResult(position=deck_point), + private=None, + state_update=state_update, ) diff --git a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py index d1527457c9c..887caf06df0 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from ..execution import PipettingHandler - from ..state import StateView + from ..state.state import StateView BlowOutInPlaceCommandType = Literal["blowOutInPlace"] diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py index 08f5f45330f..8eee75c6207 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py @@ -12,7 +12,7 @@ # Work around type-only circular dependencies. if TYPE_CHECKING: - from ...state import StateView + from ...state.state import StateView from ...types import ModuleOffsetVector, DeckSlotLocation diff --git a/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py b/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py index 81d9e30d1cc..73a0a8c2511 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from opentrons.hardware_control import HardwareControlAPI - from ...state import StateView + from ...state.state import StateView # These offsets supplied from HW _ATTACH_POINT = Point(x=0, y=110) @@ -108,10 +108,15 @@ async def execute( await ot3_api.move_axes( { Axis.Z_L: max_motion_range + _LEFT_MOUNT_Z_MARGIN, + } + ) + await ot3_api.disengage_axes([Axis.Z_L]) + await ot3_api.move_axes( + { Axis.Z_R: max_motion_range + _RIGHT_MOUNT_Z_MARGIN, } ) - await ot3_api.disengage_axes([Axis.Z_L, Axis.Z_R]) + await ot3_api.disengage_axes([Axis.Z_R]) return SuccessData(public=MoveToMaintenancePositionResult(), private=None) diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index 04846b54fc0..759606899c0 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -3,8 +3,8 @@ from __future__ import annotations +import dataclasses from abc import ABC, abstractmethod -from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import ( @@ -21,6 +21,7 @@ from pydantic.generics import GenericModel from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.state.update_types import StateUpdate from ..resources import ModelUtils from ..errors import ErrorOccurrence @@ -29,7 +30,7 @@ # Work around type-only circular dependencies. if TYPE_CHECKING: from .. import execution - from ..state import StateView + from ..state.state import StateView _ParamsT = TypeVar("_ParamsT", bound=BaseModel) @@ -106,7 +107,7 @@ class BaseCommandCreate( ) -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class SuccessData(Generic[_ResultT_co, _PrivateResultT_co]): """Data from the successful completion of a command.""" @@ -114,11 +115,22 @@ class SuccessData(Generic[_ResultT_co, _PrivateResultT_co]): """Public result data. Exposed over HTTP and stored in databases.""" private: _PrivateResultT_co - """Additional result data, only given to `opentrons.protocol_engine` internals.""" + """Additional result data, only given to `opentrons.protocol_engine` internals. + + Deprecated: + Use `state_update` instead. + """ + + state_update: StateUpdate = dataclasses.field( + # todo(mm, 2024-08-22): Remove the default once all command implementations + # use this, to make it harder to forget in new command implementations. + default_factory=StateUpdate + ) + """How the engine state should be updated to reflect this command success.""" -@dataclass(frozen=True) -class DefinedErrorData(Generic[_ErrorT_co, _PrivateResultT_co]): +@dataclasses.dataclass(frozen=True) +class DefinedErrorData(Generic[_ErrorT_co]): """Data from a command that failed with a defined error. This should only be used for "defined" errors, not any error. @@ -128,8 +140,12 @@ class DefinedErrorData(Generic[_ErrorT_co, _PrivateResultT_co]): public: _ErrorT_co """Public error data. Exposed over HTTP and stored in databases.""" - private: _PrivateResultT_co - """Additional error data, only given to `opentrons.protocol_engine` internals.""" + state_update: StateUpdate = dataclasses.field( + # todo(mm, 2024-08-22): Remove the default once all command implementations + # use this, to make it harder to forget in new command implementations. + default_factory=StateUpdate + ) + """How the engine state should be updated to reflect this command failure.""" class BaseCommand( @@ -223,9 +239,9 @@ class BaseCommand( object, ], DefinedErrorData[ - # Likewise, for our `error` field: + # Our _ImplementationCls must return public error data that can fit + # in our `error` field: _ErrorT, - object, ], ], ] @@ -236,7 +252,7 @@ class BaseCommand( "_ExecuteReturnT_co", bound=Union[ SuccessData[BaseModel, object], - DefinedErrorData[ErrorOccurrence, object], + DefinedErrorData[ErrorOccurrence], ], covariant=True, ) diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index bd45a48e7d8..b586e1f50aa 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -10,9 +10,7 @@ from .command import DefinedErrorData from .pipetting_common import ( OverpressureError, - OverpressureErrorInternalData, LiquidNotFoundError, - LiquidNotFoundErrorInternalData, ) from . import absorbance_reader @@ -216,7 +214,6 @@ PickUpTipResult, PickUpTipCommandType, TipPhysicallyMissingError, - TipPhysicallyMissingErrorInternalData, ) from .touch_tip import ( @@ -393,6 +390,7 @@ unsafe.UnsafeBlowOutInPlace, unsafe.UnsafeDropTipInPlace, unsafe.UpdatePositionEstimators, + unsafe.UnsafeEngageAxes, ], Field(discriminator="commandType"), ] @@ -467,6 +465,7 @@ unsafe.UnsafeBlowOutInPlaceParams, unsafe.UnsafeDropTipInPlaceParams, unsafe.UpdatePositionEstimatorsParams, + unsafe.UnsafeEngageAxesParams, ] CommandType = Union[ @@ -539,6 +538,7 @@ unsafe.UnsafeBlowOutInPlaceCommandType, unsafe.UnsafeDropTipInPlaceCommandType, unsafe.UpdatePositionEstimatorsCommandType, + unsafe.UnsafeEngageAxesCommandType, ] CommandCreate = Annotated[ @@ -612,6 +612,7 @@ unsafe.UnsafeBlowOutInPlaceCreate, unsafe.UnsafeDropTipInPlaceCreate, unsafe.UpdatePositionEstimatorsCreate, + unsafe.UnsafeEngageAxesCreate, ], Field(discriminator="commandType"), ] @@ -686,6 +687,7 @@ unsafe.UnsafeBlowOutInPlaceResult, unsafe.UnsafeDropTipInPlaceResult, unsafe.UpdatePositionEstimatorsResult, + unsafe.UnsafeEngageAxesResult, ] # todo(mm, 2024-06-12): Ideally, command return types would have specific @@ -701,9 +703,9 @@ # All `DefinedErrorData`s that implementations will actually return in practice. CommandDefinedErrorData = Union[ - DefinedErrorData[TipPhysicallyMissingError, TipPhysicallyMissingErrorInternalData], - DefinedErrorData[OverpressureError, OverpressureErrorInternalData], - DefinedErrorData[LiquidNotFoundError, LiquidNotFoundErrorInternalData], + DefinedErrorData[TipPhysicallyMissingError], + DefinedErrorData[OverpressureError], + DefinedErrorData[LiquidNotFoundError], ] diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index b346fb5845a..a2d0738b546 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -8,6 +8,7 @@ from pydantic import Field from ..types import DeckPoint +from ..state.update_types import StateUpdate from .pipetting_common import ( PipetteIdMixin, DispenseVolumeMixin, @@ -16,7 +17,6 @@ BaseLiquidHandlingResult, DestinationPositionResult, OverpressureError, - OverpressureErrorInternalData, ) from .command import ( AbstractCommandImpl, @@ -54,7 +54,7 @@ class DispenseResult(BaseLiquidHandlingResult, DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[DispenseResult, None], - DefinedErrorData[OverpressureError, OverpressureErrorInternalData], + DefinedErrorData[OverpressureError], ] @@ -74,12 +74,22 @@ def __init__( async def execute(self, params: DispenseParams) -> _ExecuteReturn: """Move to and dispense to the requested well.""" + state_update = StateUpdate() + position = await self._movement.move_to_well( pipette_id=params.pipetteId, labware_id=params.labwareId, well_name=params.wellName, well_location=params.wellLocation, ) + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + state_update.set_pipette_location( + pipette_id=params.pipetteId, + new_labware_id=params.labwareId, + new_well_name=params.wellName, + new_deck_point=deck_point, + ) + try: volume = await self._pipetting.dispense_in_place( pipette_id=params.pipetteId, @@ -101,19 +111,13 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: ], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - private=OverpressureErrorInternalData( - position=DeckPoint.construct( - x=position.x, y=position.y, z=position.z - ) - ), + state_update=state_update, ) else: return SuccessData( - public=DispenseResult( - volume=volume, - position=DeckPoint(x=position.x, y=position.y, z=position.z), - ), + public=DispenseResult(volume=volume, position=deck_point), private=None, + state_update=state_update, ) diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py index d71f191d1df..8f52af3284c 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py @@ -12,7 +12,6 @@ FlowRateMixin, BaseLiquidHandlingResult, OverpressureError, - OverpressureErrorInternalData, ) from .command import ( AbstractCommandImpl, @@ -22,7 +21,6 @@ DefinedErrorData, ) from ..errors.error_occurrence import ErrorOccurrence -from ..types import DeckPoint if TYPE_CHECKING: from ..execution import PipettingHandler, GantryMover @@ -49,7 +47,7 @@ class DispenseInPlaceResult(BaseLiquidHandlingResult): _ExecuteReturn = Union[ SuccessData[DispenseInPlaceResult, None], - DefinedErrorData[OverpressureError, OverpressureErrorInternalData], + DefinedErrorData[OverpressureError], ] @@ -101,13 +99,6 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: } ), ), - private=OverpressureErrorInternalData( - position=DeckPoint( - x=current_position.x, - y=current_position.y, - z=current_position.z, - ), - ), ) else: return SuccessData( diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index ddb3c56cf7e..416472fc440 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -5,13 +5,14 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal +from ..state import update_types from ..types import DropTipWellLocation, DeckPoint from .pipetting_common import PipetteIdMixin, DestinationPositionResult from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from ..state import StateView + from ..state.state import StateView from ..execution import MovementHandler, TipHandler @@ -76,6 +77,8 @@ async def execute(self, params: DropTipParams) -> SuccessData[DropTipResult, Non well_name = params.wellName home_after = params.homeAfter + state_update = update_types.StateUpdate() + if params.alternateDropLocation: well_location = self._state_view.geometry.get_next_tip_drop_location( labware_id=labware_id, @@ -101,14 +104,20 @@ async def execute(self, params: DropTipParams) -> SuccessData[DropTipResult, Non well_name=well_name, well_location=tip_drop_location, ) + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + state_update.set_pipette_location( + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, + new_deck_point=deck_point, + ) await self._tip_handler.drop_tip(pipette_id=pipette_id, home_after=home_after) return SuccessData( - public=DropTipResult( - position=DeckPoint(x=position.x, y=position.y, z=position.z) - ), + public=DropTipResult(position=deck_point), private=None, + state_update=state_update, ) diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py index b86bbc0e2ab..f9af6438958 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py index 3392ddc5a9d..fb512b72319 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py index 8c77c064282..bc06b9767c4 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler DeactivateShakerCommandType = Literal["heaterShaker/deactivateShaker"] diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py index a823f59149a..aaa36c513d7 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py @@ -8,7 +8,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler OpenLabwareLatchCommandType = Literal["heaterShaker/openLabwareLatch"] diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py index ca89166adae..2aaeac05a95 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py @@ -8,7 +8,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler SetAndWaitForShakeSpeedCommandType = Literal["heaterShaker/setAndWaitForShakeSpeed"] diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py index 9e7cfba0f33..854004dabae 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py index 981053cc459..fbd7ee24743 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index ecf932a3470..142cd93aba4 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -1,20 +1,21 @@ """The liquidProbe and tryLiquidProbe commands.""" from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Type, Union +from typing import TYPE_CHECKING, NamedTuple, Optional, Type, Union +from typing_extensions import Literal + +from pydantic import Field + from opentrons.protocol_engine.errors.exceptions import MustHomeError, TipNotEmptyError +from opentrons.protocol_engine.state import update_types from opentrons.types import MountType from opentrons_shared_data.errors.exceptions import ( PipetteLiquidNotFoundError, ) -from typing_extensions import Literal - -from pydantic import Field from ..types import DeckPoint from .pipetting_common import ( LiquidNotFoundError, - LiquidNotFoundErrorInternalData, PipetteIdMixin, WellLocationMixin, DestinationPositionResult, @@ -80,11 +81,76 @@ class TryLiquidProbeResult(DestinationPositionResult): _LiquidProbeExecuteReturn = Union[ SuccessData[LiquidProbeResult, None], - DefinedErrorData[LiquidNotFoundError, LiquidNotFoundErrorInternalData], + DefinedErrorData[LiquidNotFoundError], ] _TryLiquidProbeExecuteReturn = SuccessData[TryLiquidProbeResult, None] +class _ExecuteCommonResult(NamedTuple): + # If the probe succeeded, the z_pos that it returned. + # Or, if the probe found no liquid, the error representing that, + # so calling code can propagate those details up. + z_pos_or_error: float | PipetteLiquidNotFoundError + + state_update: update_types.StateUpdate + deck_point: DeckPoint + + +async def _execute_common( + movement: MovementHandler, pipetting: PipettingHandler, params: _CommonParams +) -> _ExecuteCommonResult: + pipette_id = params.pipetteId + labware_id = params.labwareId + well_name = params.wellName + + state_update = update_types.StateUpdate() + + # _validate_tip_attached in pipetting.py is a private method so we're using + # get_is_ready_to_aspirate as an indirect way to throw a TipNotAttachedError if appropriate + pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id) + + if pipetting.get_is_empty(pipette_id=pipette_id) is False: + raise TipNotEmptyError( + message="This operation requires a tip with no liquid in it." + ) + + if await movement.check_for_valid_position(mount=MountType.LEFT) is False: + raise MustHomeError( + message="Current position of pipette is invalid. Please home." + ) + + # liquid_probe process start position + position = await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=params.wellLocation, + ) + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + state_update.set_pipette_location( + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, + new_deck_point=deck_point, + ) + + try: + z_pos = await pipetting.liquid_probe_in_place( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=params.wellLocation, + ) + except PipetteLiquidNotFoundError as exception: + return _ExecuteCommonResult( + z_pos_or_error=exception, state_update=state_update, deck_point=deck_point + ) + else: + return _ExecuteCommonResult( + z_pos_or_error=z_pos, state_update=state_update, deck_point=deck_point + ) + + class LiquidProbeImplementation( AbstractCommandImpl[LiquidProbeParams, _LiquidProbeExecuteReturn] ): @@ -115,40 +181,10 @@ async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn: MustHomeError: as an undefined error, if the plunger is not in a valid position. """ - pipette_id = params.pipetteId - labware_id = params.labwareId - well_name = params.wellName - - # _validate_tip_attached in pipetting.py is a private method so we're using - # get_is_ready_to_aspirate as an indirect way to throw a TipNotAttachedError if appropriate - self._pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id) - - if self._pipetting.get_is_empty(pipette_id=pipette_id) is False: - raise TipNotEmptyError( - message="This operation requires a tip with no liquid in it." - ) - - if await self._movement.check_for_valid_position(mount=MountType.LEFT) is False: - raise MustHomeError( - message="Current position of pipette is invalid. Please home." - ) - - # liquid_probe process start position - position = await self._movement.move_to_well( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - well_location=params.wellLocation, + z_pos_or_error, state_update, deck_point = await _execute_common( + self._movement, self._pipetting, params ) - - try: - z_pos = await self._pipetting.liquid_probe_in_place( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - well_location=params.wellLocation, - ) - except PipetteLiquidNotFoundError as e: + if isinstance(z_pos_or_error, PipetteLiquidNotFoundError): return DefinedErrorData( public=LiquidNotFoundError( id=self._model_utils.generate_id(), @@ -157,21 +193,19 @@ async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn: ErrorOccurrence.from_failed( id=self._model_utils.generate_id(), createdAt=self._model_utils.get_timestamp(), - error=e, + error=z_pos_or_error, ) ], ), - private=LiquidNotFoundErrorInternalData( - position=DeckPoint(x=position.x, y=position.y, z=position.z) - ), + state_update=state_update, ) else: return SuccessData( public=LiquidProbeResult( - z_position=z_pos, - position=DeckPoint(x=position.x, y=position.y, z=position.z), + z_position=z_pos_or_error, position=deck_point ), private=None, + state_update=state_update, ) @@ -184,12 +218,10 @@ def __init__( self, movement: MovementHandler, pipetting: PipettingHandler, - model_utils: ModelUtils, **kwargs: object, ) -> None: self._movement = movement self._pipetting = pipetting - self._model_utils = model_utils async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: """Execute a `tryLiquidProbe` command. @@ -198,39 +230,23 @@ async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: found, `tryLiquidProbe` returns a success result with `z_position=null` instead of a defined error. """ - # We defer to the `liquidProbe` implementation. If it returns a defined - # `liquidNotFound` error, we remap that to a success result. - # Otherwise, we return the result or propagate the exception unchanged. - - original_impl = LiquidProbeImplementation( - movement=self._movement, - pipetting=self._pipetting, - model_utils=self._model_utils, + z_pos_or_error, state_update, deck_point = await _execute_common( + self._movement, self._pipetting, params + ) + + z_pos = ( + None + if isinstance(z_pos_or_error, PipetteLiquidNotFoundError) + else z_pos_or_error + ) + return SuccessData( + public=TryLiquidProbeResult( + z_position=z_pos, + position=deck_point, + ), + private=None, + state_update=state_update, ) - original_result = await original_impl.execute(params) - - match original_result: - case DefinedErrorData( - public=LiquidNotFoundError(), - private=LiquidNotFoundErrorInternalData() as original_private, - ): - return SuccessData( - public=TryLiquidProbeResult( - z_position=None, - position=original_private.position, - ), - private=None, - ) - case SuccessData( - public=LiquidProbeResult() as original_public, private=None - ): - return SuccessData( - public=TryLiquidProbeResult( - position=original_public.position, - z_position=original_public.z_position, - ), - private=None, - ) class LiquidProbe( diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index 6a4b53f4180..6b040e815da 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -19,7 +19,7 @@ from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from ..state import StateView + from ..state.state import StateView from ..execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/load_liquid.py b/api/src/opentrons/protocol_engine/commands/load_liquid.py index 02585640b0e..856cf3ee127 100644 --- a/api/src/opentrons/protocol_engine/commands/load_liquid.py +++ b/api/src/opentrons/protocol_engine/commands/load_liquid.py @@ -8,7 +8,7 @@ from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from ..state import StateView + from ..state.state import StateView LoadLiquidCommandType = Literal["loadLiquid"] diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index d4cd1efba60..e9146ccaa62 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -19,7 +19,7 @@ from opentrons.drivers.types import AbsorbanceReaderLidStatus if TYPE_CHECKING: - from ..state import StateView + from ..state.state import StateView from ..execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/load_pipette.py b/api/src/opentrons/protocol_engine/commands/load_pipette.py index ff000a30f0f..d791a251873 100644 --- a/api/src/opentrons/protocol_engine/commands/load_pipette.py +++ b/api/src/opentrons/protocol_engine/commands/load_pipette.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: from ..execution import EquipmentHandler - from ..state import StateView + from ..state.state import StateView LoadPipetteCommandType = Literal["loadPipette"] diff --git a/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py b/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py index 47a087059d5..a1be2c8480f 100644 --- a/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py +++ b/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from opentrons.protocol_engine.execution import EquipmentHandler - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView DisengageCommandType = Literal["magneticModule/disengage"] diff --git a/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py b/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py index fcedd750bc3..3796f43a022 100644 --- a/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py +++ b/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from opentrons.protocol_engine.execution import EquipmentHandler - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView EngageCommandType = Literal["magneticModule/engage"] diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 42728c05272..f02c0acde01 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from ..execution import EquipmentHandler, RunControlHandler, LabwareMovementHandler - from ..state import StateView + from ..state.state import StateView MoveLabwareCommandType = Literal["moveLabware"] diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py index 5d959538ca2..1a68b5ce570 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from ..execution import MovementHandler - from ..state import StateView + from ..state.state import StateView MoveToAddressableAreaCommandType = Literal["moveToAddressableArea"] diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py index d38d7ceb758..ca4022c9f5c 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from ..execution import MovementHandler - from ..state import StateView + from ..state.state import StateView MoveToAddressableAreaForDropTipCommandType = Literal["moveToAddressableAreaForDropTip"] diff --git a/api/src/opentrons/protocol_engine/commands/move_to_well.py b/api/src/opentrons/protocol_engine/commands/move_to_well.py index 2ed10757b69..9695ccb3bc0 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_well.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_well.py @@ -12,6 +12,7 @@ ) from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ..errors.error_occurrence import ErrorOccurrence +from ..state import update_types if TYPE_CHECKING: from ..execution import MovementHandler @@ -43,6 +44,8 @@ async def execute( self, params: MoveToWellParams ) -> SuccessData[MoveToWellResult, None]: """Move the requested pipette to the requested well.""" + state_update = update_types.StateUpdate() + x, y, z = await self._movement.move_to_well( pipette_id=params.pipetteId, labware_id=params.labwareId, @@ -52,9 +55,18 @@ async def execute( minimum_z_height=params.minimumZHeight, speed=params.speed, ) + deck_point = DeckPoint.construct(x=x, y=y, z=z) + state_update.set_pipette_location( + pipette_id=params.pipetteId, + new_labware_id=params.labwareId, + new_well_name=params.wellName, + new_deck_point=deck_point, + ) return SuccessData( - public=MoveToWellResult(position=DeckPoint(x=x, y=y, z=z)), private=None + public=MoveToWellResult(position=deck_point), + private=None, + state_update=state_update, ) diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 0022c517eeb..c30d2f953db 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -1,15 +1,14 @@ """Pick up tip command request, result, and implementation models.""" from __future__ import annotations -from dataclasses import dataclass from opentrons_shared_data.errors import ErrorCodes from pydantic import Field from typing import TYPE_CHECKING, Optional, Type, Union from typing_extensions import Literal -from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError -from ..errors import ErrorOccurrence +from ..errors import ErrorOccurrence, TipNotAttachedError from ..resources import ModelUtils +from ..state import update_types from ..types import DeckPoint from .pipetting_common import ( PipetteIdMixin, @@ -25,7 +24,7 @@ ) if TYPE_CHECKING: - from ..state import StateView + from ..state.state import StateView from ..execution import MovementHandler, TipHandler @@ -78,18 +77,9 @@ class TipPhysicallyMissingError(ErrorOccurrence): detail: str = "No tip detected." -@dataclass(frozen=True) -class TipPhysicallyMissingErrorInternalData: - """Internal-to-ProtocolEngine data about a TipPhysicallyMissingError.""" - - pipette_id: str - labware_id: str - well_name: str - - _ExecuteReturn = Union[ SuccessData[PickUpTipResult, None], - DefinedErrorData[TipPhysicallyMissingError, TipPhysicallyMissingErrorInternalData], + DefinedErrorData[TipPhysicallyMissingError], ] @@ -118,12 +108,21 @@ async def execute( well_name = params.wellName well_location = params.wellLocation + state_update = update_types.StateUpdate() + position = await self._movement.move_to_well( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, ) + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + state_update.set_pipette_location( + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, + new_deck_point=deck_point, + ) try: tip_geometry = await self._tip_handler.pick_up_tip( @@ -144,11 +143,7 @@ async def execute( ) ], ), - private=TipPhysicallyMissingErrorInternalData( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - ), + state_update=state_update, ) else: return SuccessData( @@ -156,9 +151,10 @@ async def execute( tipVolume=tip_geometry.volume, tipLength=tip_geometry.length, tipDiameter=tip_geometry.diameter, - position=DeckPoint(x=position.x, y=position.y, z=position.z), + position=deck_point, ), private=None, + state_update=state_update, ) diff --git a/api/src/opentrons/protocol_engine/commands/pipetting_common.py b/api/src/opentrons/protocol_engine/commands/pipetting_common.py index 2be1e6f2d54..29aabcb78df 100644 --- a/api/src/opentrons/protocol_engine/commands/pipetting_common.py +++ b/api/src/opentrons/protocol_engine/commands/pipetting_common.py @@ -1,5 +1,4 @@ """Common pipetting command base models.""" -from dataclasses import dataclass from opentrons_shared_data.errors import ErrorCodes from pydantic import BaseModel, Field from typing import Literal, Optional, Tuple, TypedDict @@ -114,6 +113,14 @@ class BaseLiquidHandlingResult(BaseModel): class DestinationPositionResult(BaseModel): """Mixin for command results that move a pipette.""" + # todo(mm, 2024-08-02): Consider deprecating or redefining this. + # + # This is here because opentrons.protocol_engine needed it for internal bookkeeping + # and, at the time, we didn't have a way to do that without adding this to the + # public command results. Its usefulness to callers outside + # opentrons.protocol_engine is questionable because they would need to know which + # critical point is in play, and I think that can change depending on obscure + # things like labware quirks. position: DeckPoint = Field( DeckPoint(x=0, y=0, z=0), description=( @@ -149,14 +156,6 @@ class OverpressureError(ErrorOccurrence): errorInfo: ErrorLocationInfo -@dataclass(frozen=True) -class OverpressureErrorInternalData: - """Internal-to-ProtocolEngine data about an OverpressureError.""" - - position: DeckPoint - """Same meaning as DestinationPositionResult.position.""" - - class LiquidNotFoundError(ErrorOccurrence): """Returned when no liquid is detected during the liquid probe process/move. @@ -169,11 +168,3 @@ class LiquidNotFoundError(ErrorOccurrence): errorCode: str = ErrorCodes.PIPETTE_LIQUID_NOT_FOUND.value.code detail: str = ErrorCodes.PIPETTE_LIQUID_NOT_FOUND.value.detail - - -@dataclass(frozen=True) -class LiquidNotFoundErrorInternalData: - """Internal-to-ProtocolEngine data about a LiquidNotFoundError.""" - - position: DeckPoint - """Same meaning as DestinationPositionResult.position.""" diff --git a/api/src/opentrons/protocol_engine/commands/reload_labware.py b/api/src/opentrons/protocol_engine/commands/reload_labware.py index 884b8324d21..116698552cd 100644 --- a/api/src/opentrons/protocol_engine/commands/reload_labware.py +++ b/api/src/opentrons/protocol_engine/commands/reload_labware.py @@ -8,7 +8,7 @@ from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from ..state import StateView + from ..state.state import StateView from ..execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py b/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py index 979195933b2..52e988b179d 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler DeactivateTemperatureCommandType = Literal["temperatureModule/deactivate"] diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py b/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py index 4302773722b..7e76de7d561 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler SetTargetTemperatureCommandType = Literal["temperatureModule/setTargetTemperature"] diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py b/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py index 9abd6d13179..7a96be35242 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler WaitForTemperatureCommandType = Literal["temperatureModule/waitForTemperature"] diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py index de7768c4c7a..1e0761d03ab 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py @@ -10,7 +10,7 @@ from opentrons.protocol_engine.types import MotorAxis if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py index a24706a54c3..fd108dc9568 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py index 4f76d2c3d3e..ff0fabc1e88 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py index 0facf0d4ec3..762cf9172ed 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py @@ -10,7 +10,7 @@ from opentrons.protocol_engine.types import MotorAxis if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py b/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py index af387e3324e..c0b5189afcb 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py @@ -11,7 +11,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py index 796fb15c024..587369b733b 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py index a819d6a3759..5e7efa6bfd2 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py index 40a8241adaa..dabe351f352 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py index 026aed14ad6..d15eb4f3238 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py @@ -9,7 +9,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/touch_tip.py b/api/src/opentrons/protocol_engine/commands/touch_tip.py index 858be81842c..744b1c14107 100644 --- a/api/src/opentrons/protocol_engine/commands/touch_tip.py +++ b/api/src/opentrons/protocol_engine/commands/touch_tip.py @@ -4,6 +4,8 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal +from opentrons.protocol_engine.state import update_types + from ..errors import TouchTipDisabledError, LabwareIsTipRackError from ..types import DeckPoint from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData @@ -16,7 +18,7 @@ if TYPE_CHECKING: from ..execution import MovementHandler, GantryMover - from ..state import StateView + from ..state.state import StateView TouchTipCommandType = Literal["touchTip"] @@ -71,6 +73,8 @@ async def execute( labware_id = params.labwareId well_name = params.wellName + state_update = update_types.StateUpdate() + if self._state_view.labware.get_has_quirk(labware_id, "touchTipDisabled"): raise TouchTipDisabledError( f"Touch tip not allowed on labware {labware_id}" @@ -98,14 +102,25 @@ async def execute( center_point=center_point, ) - x, y, z = await self._gantry_mover.move_to( + final_point = await self._gantry_mover.move_to( pipette_id=pipette_id, waypoints=touch_waypoints, speed=touch_speed, ) + final_deck_point = DeckPoint.construct( + x=final_point.x, y=final_point.y, z=final_point.z + ) + state_update.set_pipette_location( + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, + new_deck_point=final_deck_point, + ) return SuccessData( - public=TouchTipResult(position=DeckPoint(x=x, y=y, z=z)), private=None + public=TouchTipResult(position=final_deck_point), + private=None, + state_update=state_update, ) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py b/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py index 2875d38cb8e..6b92cc2e18e 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py @@ -23,6 +23,14 @@ UpdatePositionEstimatorsCreate, ) +from .unsafe_engage_axes import ( + UnsafeEngageAxesCommandType, + UnsafeEngageAxesParams, + UnsafeEngageAxesResult, + UnsafeEngageAxes, + UnsafeEngageAxesCreate, +) + __all__ = [ # Unsafe blow-out-in-place command models "UnsafeBlowOutInPlaceCommandType", @@ -42,4 +50,10 @@ "UpdatePositionEstimatorsResult", "UpdatePositionEstimators", "UpdatePositionEstimatorsCreate", + # Unsafe engage axes + "UnsafeEngageAxesCommandType", + "UnsafeEngageAxesParams", + "UnsafeEngageAxesResult", + "UnsafeEngageAxes", + "UnsafeEngageAxesCreate", ] diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py index cbf17ff1026..d9ef8e1d15d 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from ...execution import PipettingHandler - from ...state import StateView + from ...state.state import StateView UnsafeBlowOutInPlaceCommandType = Literal["unsafe/blowOutInPlace"] diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py index 2cb3fa78dd8..6bf2d4a3a3f 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from ...execution import TipHandler - from ...state import StateView + from ...state.state import StateView UnsafeDropTipInPlaceCommandType = Literal["unsafe/dropTipInPlace"] diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py new file mode 100644 index 00000000000..500347d84b0 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py @@ -0,0 +1,83 @@ +"""Update position estimators payload, result, and implementaiton.""" + +from __future__ import annotations +from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Optional, List, Type +from typing_extensions import Literal + +from ...types import MotorAxis +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence +from ...resources import ensure_ot3_hardware + +from opentrons.hardware_control import HardwareControlAPI + +if TYPE_CHECKING: + from ...execution import GantryMover + + +UnsafeEngageAxesCommandType = Literal["unsafe/engageAxes"] + + +class UnsafeEngageAxesParams(BaseModel): + """Payload required for an UnsafeEngageAxes command.""" + + axes: List[MotorAxis] = Field(..., description="The axes for which to enable.") + + +class UnsafeEngageAxesResult(BaseModel): + """Result data from the execution of an UnsafeEngageAxes command.""" + + +class UnsafeEngageAxesImplementation( + AbstractCommandImpl[ + UnsafeEngageAxesParams, + SuccessData[UnsafeEngageAxesResult, None], + ] +): + """Enable axes command implementation.""" + + def __init__( + self, + hardware_api: HardwareControlAPI, + gantry_mover: GantryMover, + **kwargs: object, + ) -> None: + self._hardware_api = hardware_api + self._gantry_mover = gantry_mover + + async def execute( + self, params: UnsafeEngageAxesParams + ) -> SuccessData[UnsafeEngageAxesResult, None]: + """Enable exes.""" + ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) + await ot3_hardware_api.engage_axes( + [ + self._gantry_mover.motor_axis_to_hardware_axis(axis) + for axis in params.axes + ] + ) + return SuccessData(public=UnsafeEngageAxesResult(), private=None) + + +class UnsafeEngageAxes( + BaseCommand[UnsafeEngageAxesParams, UnsafeEngageAxesResult, ErrorOccurrence] +): + """UnsafeEngageAxes command model.""" + + commandType: UnsafeEngageAxesCommandType = "unsafe/engageAxes" + params: UnsafeEngageAxesParams + result: Optional[UnsafeEngageAxesResult] + + _ImplementationCls: Type[ + UnsafeEngageAxesImplementation + ] = UnsafeEngageAxesImplementation + + +class UnsafeEngageAxesCreate(BaseCommandCreate[UnsafeEngageAxesParams]): + """UnsafeEngageAxes command request model.""" + + commandType: UnsafeEngageAxesCommandType = "unsafe/engageAxes" + params: UnsafeEngageAxesParams + + _CommandCls: Type[UnsafeEngageAxes] = UnsafeEngageAxes diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index bf2157c83a5..d3d50da14df 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -11,7 +11,8 @@ from .protocol_engine import ProtocolEngine from .resources import DeckDataProvider, ModuleDataProvider -from .state import Config, StateStore +from .state.config import Config +from .state.state import StateStore from .types import PostRunHardwareState, DeckConfigurationType from .engine_support import create_run_orchestrator diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 639648e820f..e0f60a5cd45 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -69,6 +69,9 @@ InvalidAxisForRobotType, NotSupportedOnRobotType, CommandNotAllowedError, + InvalidLiquidHeightFound, + LiquidHeightUnknownError, + InvalidWellDefinitionError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -146,4 +149,7 @@ # error occurrence models "ErrorOccurrence", "CommandNotAllowedError", + "InvalidLiquidHeightFound", + "LiquidHeightUnknownError", + "InvalidWellDefinitionError", ] diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 8d8ed34fb9f..57d420124a7 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -1002,6 +1002,32 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class InvalidLiquidHeightFound(ProtocolEngineError): + """Raised when attempting to estimate liquid height based on volume fails.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an InvalidLiquidHeightFound error.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class LiquidHeightUnknownError(ProtocolEngineError): + """Raised when attempting to specify WellOrigin.MENISCUS before liquid probing has been done.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a LiquidHeightUnknownError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class EStopActivatedError(ProtocolEngineError): """Represents an E-stop event.""" @@ -1043,3 +1069,16 @@ def __init__( ) -> None: """Build a TipNotEmptyError.""" super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class InvalidWellDefinitionError(ProtocolEngineError): + """Raised when an InnerWellGeometry definition is invalid.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an InvalidWellDefinitionError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index e427b945e0d..e9dd2ec73b9 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -13,7 +13,7 @@ from opentrons.protocol_engine.commands.command import SuccessData -from ..state import StateStore +from ..state.state import StateStore from ..resources import ModelUtils from ..commands import CommandStatus from ..actions import ( @@ -185,7 +185,9 @@ async def execute(self, command_id: str) -> None: succeeded_command = running_command.copy(update=update) self._action_dispatcher.dispatch( SucceedCommandAction( - command=succeeded_command, private_result=result.private + command=succeeded_command, + private_result=result.private, + state_update=result.state_update, ), ) else: diff --git a/api/src/opentrons/protocol_engine/execution/create_queue_worker.py b/api/src/opentrons/protocol_engine/execution/create_queue_worker.py index 3596ce6d96e..e449a013008 100644 --- a/api/src/opentrons/protocol_engine/execution/create_queue_worker.py +++ b/api/src/opentrons/protocol_engine/execution/create_queue_worker.py @@ -4,7 +4,7 @@ from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.execution.rail_lights import RailLightsHandler -from ..state import StateStore +from ..state.state import StateStore from ..actions import ActionDispatcher from .equipment import EquipmentHandler from .movement import MovementHandler diff --git a/api/src/opentrons/protocol_engine/execution/door_watcher.py b/api/src/opentrons/protocol_engine/execution/door_watcher.py index b35e73bdab9..a14712d4837 100644 --- a/api/src/opentrons/protocol_engine/execution/door_watcher.py +++ b/api/src/opentrons/protocol_engine/execution/door_watcher.py @@ -15,7 +15,7 @@ from opentrons.protocol_engine.actions import ActionDispatcher, DoorChangeAction -from ..state import StateStore +from ..state.state import StateStore _UnsubscribeCallback = Callable[[], None] diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index 4093c93489c..792bd583b88 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -35,7 +35,8 @@ ModelUtils, pipette_data_provider, ) -from ..state import StateStore, HardwareModule +from ..state.state import StateStore +from ..state.modules import HardwareModule from ..types import ( LabwareLocation, DeckSlotLocation, diff --git a/api/src/opentrons/protocol_engine/execution/gantry_mover.py b/api/src/opentrons/protocol_engine/execution/gantry_mover.py index 26ab20f69de..98a3d19b8d5 100644 --- a/api/src/opentrons/protocol_engine/execution/gantry_mover.py +++ b/api/src/opentrons/protocol_engine/execution/gantry_mover.py @@ -10,7 +10,7 @@ from opentrons.motion_planning import Waypoint -from ..state import StateView +from ..state.state import StateView from ..types import MotorAxis, CurrentWell from ..errors import MustHomeError, InvalidAxisForRobotType diff --git a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py index 28eacd7525b..28c310acd70 100644 --- a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py +++ b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py @@ -6,7 +6,7 @@ from opentrons.types import PipetteNotAttachedError as HwPipetteNotAttachedError from ..resources.ot3_validation import ensure_ot3_hardware -from ..state import StateStore +from ..state.state import StateStore from ..types import MotorAxis, PostRunHardwareState from ..errors import HardwareNotSupportedError diff --git a/api/src/opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py b/api/src/opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py index d5ddbf81554..78b8f2e9bfa 100644 --- a/api/src/opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +++ b/api/src/opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py @@ -13,7 +13,7 @@ HeaterShakerLabwareLatchStatusUnknown, WrongModuleTypeError, ) -from ..state import StateStore +from ..state.state import StateStore from ..state.module_substates import HeaterShakerModuleSubState from ..types import ( HeaterShakerMovementRestrictors, diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py index 3cdd78b8808..5851cacd7b3 100644 --- a/api/src/opentrons/protocol_engine/execution/labware_movement.py +++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py @@ -9,7 +9,7 @@ from opentrons.hardware_control.types import OT3Mount, Axis from opentrons.motion_planning import get_gripper_labware_movement_waypoints -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.resources.ot3_validation import ensure_ot3_hardware from .thermocycler_movement_flagger import ThermocyclerMovementFlagger diff --git a/api/src/opentrons/protocol_engine/execution/movement.py b/api/src/opentrons/protocol_engine/execution/movement.py index 451f482ad0d..ae4fe27db10 100644 --- a/api/src/opentrons/protocol_engine/execution/movement.py +++ b/api/src/opentrons/protocol_engine/execution/movement.py @@ -16,7 +16,7 @@ CurrentWell, AddressableOffsetVector, ) -from ..state import StateStore +from ..state.state import StateStore from ..resources import ModelUtils from .thermocycler_movement_flagger import ThermocyclerMovementFlagger from .heater_shaker_movement_flagger import HeaterShakerMovementFlagger diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index c3e606849ff..fed6fc52ee6 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -5,7 +5,8 @@ from opentrons.hardware_control import HardwareControlAPI -from ..state import StateView, HardwarePipette +from ..state.state import StateView +from ..state.pipettes import HardwarePipette from ..notes import CommandNoteAdder, CommandNote from ..errors.exceptions import ( TipNotAttachedError, @@ -193,7 +194,9 @@ async def liquid_probe_in_place( mount=hw_pipette.mount, max_z_dist=well_depth - lld_min_height + well_location.offset.z, ) - return float(z_pos) + labware_pos = self._state_view.geometry.get_labware_position(labware_id) + relative_height = z_pos - labware_pos.z - well_def.z + return float(relative_height) @contextmanager def _set_flow_rate( diff --git a/api/src/opentrons/protocol_engine/execution/queue_worker.py b/api/src/opentrons/protocol_engine/execution/queue_worker.py index fc2eceebc96..67f8f17b42c 100644 --- a/api/src/opentrons/protocol_engine/execution/queue_worker.py +++ b/api/src/opentrons/protocol_engine/execution/queue_worker.py @@ -3,7 +3,7 @@ from logging import getLogger from typing import Optional, AsyncGenerator, Callable -from ..state import StateStore +from ..state.state import StateStore from .command_executor import CommandExecutor log = getLogger(__name__) diff --git a/api/src/opentrons/protocol_engine/execution/run_control.py b/api/src/opentrons/protocol_engine/execution/run_control.py index 23220742405..1525353ac60 100644 --- a/api/src/opentrons/protocol_engine/execution/run_control.py +++ b/api/src/opentrons/protocol_engine/execution/run_control.py @@ -1,7 +1,7 @@ """Run control command side-effect logic.""" import asyncio -from ..state import StateStore +from ..state.state import StateStore from ..actions import ActionDispatcher, PauseAction, PauseSource diff --git a/api/src/opentrons/protocol_engine/execution/thermocycler_movement_flagger.py b/api/src/opentrons/protocol_engine/execution/thermocycler_movement_flagger.py index 463a896c1bc..742bc6b4278 100644 --- a/api/src/opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +++ b/api/src/opentrons/protocol_engine/execution/thermocycler_movement_flagger.py @@ -7,7 +7,7 @@ from opentrons.hardware_control.modules import Thermocycler as HardwareThermocycler from ..types import ModuleLocation, LabwareLocation -from ..state import StateStore +from ..state.state import StateStore from ..errors import ThermocyclerNotOpenError, WrongModuleTypeError diff --git a/api/src/opentrons/protocol_engine/execution/thermocycler_plate_lifter.py b/api/src/opentrons/protocol_engine/execution/thermocycler_plate_lifter.py index 5691312bba8..1118dcc91bd 100644 --- a/api/src/opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +++ b/api/src/opentrons/protocol_engine/execution/thermocycler_plate_lifter.py @@ -5,7 +5,8 @@ from typing import TYPE_CHECKING, AsyncGenerator, Optional from opentrons.hardware_control.modules.thermocycler import Thermocycler from opentrons.protocol_engine.types import LabwareLocation, ModuleLocation, ModuleModel -from opentrons.protocol_engine.state import StateStore, ThermocyclerModuleId +from opentrons.protocol_engine.state.state import StateStore +from opentrons.protocol_engine.state.module_substates import ThermocyclerModuleId from contextlib import asynccontextmanager if TYPE_CHECKING: diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index 7acfae1e3ef..3968c7a6923 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -11,7 +11,7 @@ ) from ..resources import LabwareDataProvider, ensure_ot3_hardware -from ..state import StateView +from ..state.state import StateView from ..types import TipGeometry, TipPresenceStatus from ..errors import ( HardwareNotSupportedError, diff --git a/api/src/opentrons/protocol_engine/plugins.py b/api/src/opentrons/protocol_engine/plugins.py index d729302aea7..da900bb9929 100644 --- a/api/src/opentrons/protocol_engine/plugins.py +++ b/api/src/opentrons/protocol_engine/plugins.py @@ -5,7 +5,7 @@ from typing_extensions import final from .actions import Action, ActionDispatcher, ActionHandler -from .state import StateView +from .state.state import StateView class AbstractPlugin(ActionHandler, ABC): diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index ffb251166cd..c5219e889a3 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -38,7 +38,7 @@ DoorWatcher, HardwareStopper, ) -from .state import StateStore, StateView +from .state.state import StateStore, StateView from .plugins import AbstractPlugin, PluginStarter from .actions import ( ActionDispatcher, diff --git a/api/src/opentrons/protocol_engine/state/__init__.py b/api/src/opentrons/protocol_engine/state/__init__.py index f9705905967..00043706a6c 100644 --- a/api/src/opentrons/protocol_engine/state/__init__.py +++ b/api/src/opentrons/protocol_engine/state/__init__.py @@ -1,71 +1 @@ """Protocol engine state module.""" - -from .state import State, StateStore, StateView -from .state_summary import StateSummary -from .config import Config -from .commands import ( - CommandState, - CommandView, - CommandSlice, - CommandErrorSlice, - CommandPointer, -) -from .command_history import CommandEntry -from .labware import LabwareState, LabwareView -from .pipettes import PipetteState, PipetteView, HardwarePipette -from .modules import ModuleState, ModuleView, HardwareModule -from .module_substates import ( - MagneticModuleId, - MagneticModuleSubState, - HeaterShakerModuleId, - HeaterShakerModuleSubState, - TemperatureModuleId, - TemperatureModuleSubState, - ThermocyclerModuleId, - ThermocyclerModuleSubState, - ModuleSubStateType, -) -from .geometry import GeometryView -from .motion import MotionView, PipetteLocationData - -__all__ = [ - # top level state value and interfaces - "State", - "StateStore", - "StateView", - "StateSummary", - # static engine configuration - "Config", - # command state and values - "CommandState", - "CommandView", - "CommandSlice", - "CommandErrorSlice", - "CommandPointer", - "CommandEntry", - # labware state and values - "LabwareState", - "LabwareView", - # pipette state and values - "PipetteState", - "PipetteView", - "HardwarePipette", - # module state and values - "ModuleState", - "ModuleView", - "HardwareModule", - "MagneticModuleId", - "MagneticModuleSubState", - "HeaterShakerModuleId", - "HeaterShakerModuleSubState", - "TemperatureModuleId", - "TemperatureModuleSubState", - "ThermocyclerModuleId", - "ThermocyclerModuleSubState", - "ModuleSubStateType", - # computed geometry state - "GeometryView", - # computed motion state - "MotionView", - "PipetteLocationData", -] diff --git a/api/src/opentrons/protocol_engine/state/abstract_store.py b/api/src/opentrons/protocol_engine/state/_abstract_store.py similarity index 100% rename from api/src/opentrons/protocol_engine/state/abstract_store.py rename to api/src/opentrons/protocol_engine/state/_abstract_store.py diff --git a/api/src/opentrons/protocol_engine/state/move_types.py b/api/src/opentrons/protocol_engine/state/_move_types.py similarity index 100% rename from api/src/opentrons/protocol_engine/state/move_types.py rename to api/src/opentrons/protocol_engine/state/_move_types.py diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index ab9c3d8462d..afd076380f7 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -43,7 +43,7 @@ AddAddressableAreaAction, ) from .config import Config -from .abstract_store import HasState, HandlesActions +from ._abstract_store import HasState, HandlesActions @dataclass @@ -352,6 +352,20 @@ def mount_offsets(self) -> Dict[str, Point]: "right": Point(x=right_offset[0], y=right_offset[1], z=right_offset[2]), } + @cached_property + def padding_offsets(self) -> Dict[str, float]: + """The padding offsets to be applied to the deck extents of the robot.""" + rear_offset = self.state.robot_definition["paddingOffsets"]["rear"] + front_offset = self.state.robot_definition["paddingOffsets"]["front"] + left_side_offset = self.state.robot_definition["paddingOffsets"]["leftSide"] + right_side_offset = self.state.robot_definition["paddingOffsets"]["rightSide"] + return { + "rear": rear_offset, + "front": front_offset, + "left_side": left_side_offset, + "right_side": right_side_offset, + } + def get_addressable_area(self, addressable_area_name: str) -> AddressableArea: """Get addressable area.""" if not self._state.use_simulated_deck_config: diff --git a/api/src/opentrons/protocol_engine/state/command_history.py b/api/src/opentrons/protocol_engine/state/command_history.py index adebadd64bc..d555764e54e 100644 --- a/api/src/opentrons/protocol_engine/state/command_history.py +++ b/api/src/opentrons/protocol_engine/state/command_history.py @@ -24,6 +24,9 @@ class CommandHistory: _all_command_ids: List[str] """All command IDs, in insertion order.""" + _all_command_ids_but_fixit_command_ids: List[str] + """All command IDs besides fixit command intents, in insertion order.""" + _commands_by_id: Dict[str, CommandEntry] """All command resources, in insertion order, mapped by their unique IDs.""" @@ -44,6 +47,7 @@ class CommandHistory: def __init__(self) -> None: self._all_command_ids = [] + self._all_command_ids_but_fixit_command_ids = [] self._queued_command_ids = OrderedSet() self._queued_setup_command_ids = OrderedSet() self._queued_fixit_command_ids = OrderedSet() @@ -97,13 +101,26 @@ def get_all_commands(self) -> List[Command]: for command_id in self._all_command_ids ] + def get_filtered_command_ids(self, include_fixit_commands: bool) -> List[str]: + """Get all fixit command IDs.""" + if include_fixit_commands: + return self._all_command_ids + else: + return self._all_command_ids_but_fixit_command_ids + def get_all_ids(self) -> List[str]: """Get all command IDs.""" return self._all_command_ids - def get_slice(self, start: int, stop: int) -> List[Command]: - """Get a list of commands between start and stop.""" + def get_slice( + self, start: int, stop: int, command_ids: Optional[list[str]] = None + ) -> List[Command]: + """Get a list of commands between start and stop.""" """Get a list of commands between start and stop.""" commands = self._all_command_ids[start:stop] + selected_command_ids = ( + command_ids if command_ids is not None else self._all_command_ids + ) + commands = selected_command_ids[start:stop] return [self._commands_by_id[command].command for command in commands] def get_tail_command(self) -> Optional[CommandEntry]: @@ -230,6 +247,8 @@ def _add(self, command_id: str, command_entry: CommandEntry) -> None: """Create or update a command entry.""" if command_id not in self._commands_by_id: self._all_command_ids.append(command_id) + if command_entry.command.intent != CommandIntent.FIXIT: + self._all_command_ids_but_fixit_command_ids.append(command_id) self._commands_by_id[command_id] = command_entry def _add_to_queue(self, command_id: str) -> None: diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index c725c561ac3..d01926862de 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -49,7 +49,7 @@ ProtocolCommandFailedError, ) from ..types import EngineStatus -from .abstract_store import HasState, HandlesActions +from ._abstract_store import HasState, HandlesActions from .command_history import ( CommandEntry, CommandHistory, @@ -580,18 +580,19 @@ def get_all(self) -> List[Command]: return self._state.command_history.get_all_commands() def get_slice( - self, - cursor: Optional[int], - length: int, + self, cursor: Optional[int], length: int, include_fixit_commands: bool ) -> CommandSlice: """Get a subset of commands around a given cursor. If the cursor is omitted, a cursor will be selected automatically based on the currently running or most recently executed command. """ + command_ids = self._state.command_history.get_filtered_command_ids( + include_fixit_commands=include_fixit_commands + ) running_command = self._state.command_history.get_running_command() queued_command_ids = self._state.command_history.get_queue_ids() - total_length = self._state.command_history.length() + total_length = len(command_ids) # TODO(mm, 2024-05-17): This looks like it's attempting to do the same thing # as self.get_current(), but in a different way. Can we unify them? @@ -620,7 +621,9 @@ def get_slice( # start is inclusive, stop is exclusive actual_cursor = max(0, min(cursor, total_length - 1)) stop = min(total_length, actual_cursor + length) - commands = self._state.command_history.get_slice(start=actual_cursor, stop=stop) + commands = self._state.command_history.get_slice( + start=actual_cursor, stop=stop, command_ids=command_ids + ) return CommandSlice( commands=commands, diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py new file mode 100644 index 00000000000..b78957a2f5f --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -0,0 +1,243 @@ +"""Helper functions for liquid-level related calculations inside a given frustum.""" +from typing import List, Tuple, Iterator, Sequence, Any +from numpy import pi, iscomplex, roots, real + +from ..errors.exceptions import InvalidLiquidHeightFound +from opentrons_shared_data.labware.types import ( + is_circular_frusta_list, + is_rectangular_frusta_list, +) +from opentrons_shared_data.labware.labware_definition import InnerWellGeometry + + +def reject_unacceptable_heights( + potential_heights: List[float], max_height: float +) -> float: + """Reject any solutions to a polynomial equation that cannot be the height of a frustum.""" + valid_heights = [] + for root in potential_heights: + # reject any heights that are negative or greater than the max height + if not iscomplex(root): + # take only the real component of the root and round to 4 decimal places + rounded_root = round(real(root), 4) + if (rounded_root <= max_height) and (rounded_root >= 0): + valid_heights.append(rounded_root) + if len(valid_heights) != 1: + raise InvalidLiquidHeightFound( + message="Unable to estimate valid liquid height from volume." + ) + return valid_heights[0] + + +def rectangular_frustum_polynomial_roots( + bottom_length: float, + bottom_width: float, + top_length: float, + top_width: float, + total_frustum_height: float, +) -> Tuple[float, float, float]: + """Polynomial representation of the volume of a rectangular frustum.""" + # roots of the polynomial with shape ax^3 + bx^2 + cx + a = ( + (top_length - bottom_length) + * (top_width - bottom_width) + / (3 * total_frustum_height**2) + ) + b = ( + (bottom_length * (top_width - bottom_width)) + + (bottom_width * (top_length - bottom_length)) + ) / (2 * total_frustum_height) + c = bottom_length * bottom_width + return a, b, c + + +def circular_frustum_polynomial_roots( + bottom_radius: float, + top_radius: float, + total_frustum_height: float, +) -> Tuple[float, float, float]: + """Polynomial representation of the volume of a circular frustum.""" + # roots of the polynomial with shape ax^3 + bx^2 + cx + a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_frustum_height**2) + b = pi * bottom_radius * (top_radius - bottom_radius) / total_frustum_height + c = pi * bottom_radius**2 + return a, b, c + + +def volume_from_height_circular( + target_height: float, + total_frustum_height: float, + bottom_radius: float, + top_radius: float, +) -> float: + """Find the volume given a height within a circular frustum.""" + a, b, c = circular_frustum_polynomial_roots( + bottom_radius=bottom_radius, + top_radius=top_radius, + total_frustum_height=total_frustum_height, + ) + volume = a * (target_height**3) + b * (target_height**2) + c * target_height + return volume + + +def volume_from_height_rectangular( + target_height: float, + total_frustum_height: float, + bottom_length: float, + bottom_width: float, + top_length: float, + top_width: float, +) -> float: + """Find the volume given a height within a rectangular frustum.""" + a, b, c = rectangular_frustum_polynomial_roots( + bottom_length=bottom_length, + bottom_width=bottom_width, + top_length=top_length, + top_width=top_width, + total_frustum_height=total_frustum_height, + ) + volume = a * (target_height**3) + b * (target_height**2) + c * target_height + return volume + + +def volume_from_height_spherical( + target_height: float, + radius_of_curvature: float, +) -> float: + """Find the volume given a height within a spherical frustum.""" + volume = ( + (1 / 3) * pi * (target_height**2) * (3 * radius_of_curvature - target_height) + ) + return volume + + +def height_from_volume_circular( + volume: float, + total_frustum_height: float, + bottom_radius: float, + top_radius: float, +) -> float: + """Find the height given a volume within a circular frustum.""" + a, b, c = circular_frustum_polynomial_roots( + bottom_radius=bottom_radius, + top_radius=top_radius, + total_frustum_height=total_frustum_height, + ) + d = volume * -1 + x_intercept_roots = (a, b, c, d) + + height_from_volume_roots = roots(x_intercept_roots) + height = reject_unacceptable_heights( + potential_heights=list(height_from_volume_roots), + max_height=total_frustum_height, + ) + return height + + +def height_from_volume_rectangular( + volume: float, + total_frustum_height: float, + bottom_length: float, + bottom_width: float, + top_length: float, + top_width: float, +) -> float: + """Find the height given a volume within a rectangular frustum.""" + a, b, c = rectangular_frustum_polynomial_roots( + bottom_length=bottom_length, + bottom_width=bottom_width, + top_length=top_length, + top_width=top_width, + total_frustum_height=total_frustum_height, + ) + d = volume * -1 + x_intercept_roots = (a, b, c, d) + + height_from_volume_roots = roots(x_intercept_roots) + height = reject_unacceptable_heights( + potential_heights=list(height_from_volume_roots), + max_height=total_frustum_height, + ) + return height + + +def height_from_volume_spherical( + volume: float, + radius_of_curvature: float, + total_frustum_height: float, +) -> float: + """Find the height given a volume within a spherical frustum.""" + a = -1 * pi / 3 + b = pi * radius_of_curvature + c = 0.0 + d = volume * -1 + x_intercept_roots = (a, b, c, d) + + height_from_volume_roots = roots(x_intercept_roots) + height = reject_unacceptable_heights( + potential_heights=list(height_from_volume_roots), + max_height=total_frustum_height, + ) + return height + + +def get_boundary_cross_sections(frusta: Sequence[Any]) -> Iterator[Tuple[Any, Any]]: + """Yield tuples representing two cross-section boundaries of a segment of a well.""" + iter_f = iter(frusta) + el = next(iter_f) + for next_el in iter_f: + yield el, next_el + el = next_el + + +def get_well_volumetric_capacity( + well_geometry: InnerWellGeometry, +) -> List[Tuple[float, float]]: + """Return the total volumetric capacity of a well as a map of height borders to volume.""" + # dictionary map of heights to volumetric capacities within their respective segment + # {top_height_0: volume_0, top_height_1: volume_1, top_height_2: volume_2} + well_volume = [] + if well_geometry.bottomShape is not None: + if well_geometry.bottomShape.shape == "spherical": + bottom_spherical_section_depth = well_geometry.bottomShape.depth + bottom_sphere_volume = volume_from_height_spherical( + radius_of_curvature=well_geometry.bottomShape.radius_of_curvature, + target_height=bottom_spherical_section_depth, + ) + well_volume.append((bottom_spherical_section_depth, bottom_sphere_volume)) + + # get the volume of remaining frusta sorted in ascending order + sorted_frusta = sorted(well_geometry.frusta, key=lambda section: section.topHeight) + + if is_rectangular_frusta_list(sorted_frusta): + for f, next_f in get_boundary_cross_sections(sorted_frusta): + top_cross_section_width = next_f["xDimension"] + top_cross_section_length = next_f["yDimension"] + bottom_cross_section_width = f["xDimension"] + bottom_cross_section_length = f["yDimension"] + frustum_height = next_f["topHeight"] - f["topHeight"] + frustum_volume = volume_from_height_rectangular( + target_height=frustum_height, + total_frustum_height=frustum_height, + bottom_length=bottom_cross_section_length, + bottom_width=bottom_cross_section_width, + top_length=top_cross_section_length, + top_width=top_cross_section_width, + ) + + well_volume.append((next_f["topHeight"], frustum_volume)) + elif is_circular_frusta_list(sorted_frusta): + for f, next_f in get_boundary_cross_sections(sorted_frusta): + top_cross_section_radius = next_f["diameter"] / 2.0 + bottom_cross_section_radius = f["diameter"] / 2.0 + frustum_height = next_f["topHeight"] - f["topHeight"] + frustum_volume = volume_from_height_circular( + target_height=frustum_height, + total_frustum_height=frustum_height, + bottom_radius=bottom_cross_section_radius, + top_radius=top_cross_section_radius, + ) + + well_volume.append((next_f["topHeight"], frustum_volume)) + + return well_volume diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 9be6f7e5952..a0fef65e7ee 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -18,6 +18,7 @@ LabwareNotLoadedOnLabwareError, LabwareNotLoadedOnModuleError, LabwareMovementNotAllowedError, + InvalidWellDefinitionError, ) from ..resources import fixture_validation from ..types import ( @@ -48,9 +49,11 @@ ) from .config import Config from .labware import LabwareView +from .wells import WellView from .modules import ModuleView from .pipettes import PipetteView from .addressable_areas import AddressableAreaView +from .frustum_helpers import get_well_volumetric_capacity SLOT_WIDTH = 128 @@ -77,6 +80,11 @@ class _GripperMoveType(enum.Enum): class _AbsoluteRobotExtents: front_left: Dict[MountType, Point] back_right: Dict[MountType, Point] + deck_extents: Point + padding_rear: float + padding_front: float + padding_left_side: float + padding_right_side: float _LabwareLocation = TypeVar("_LabwareLocation", bound=LabwareLocation) @@ -91,6 +99,7 @@ def __init__( self, config: Config, labware_view: LabwareView, + well_view: WellView, module_view: ModuleView, pipette_view: PipetteView, addressable_area_view: AddressableAreaView, @@ -98,6 +107,7 @@ def __init__( """Initialize a GeometryView instance.""" self._config = config self._labware = labware_view + self._wells = well_view self._modules = module_view self._pipettes = pipette_view self._addressable_areas = addressable_area_view @@ -118,7 +128,13 @@ def absolute_deck_extents(self) -> _AbsoluteRobotExtents: MountType.RIGHT: self._addressable_areas.deck_extents + right_offset, } return _AbsoluteRobotExtents( - front_left=front_left_abs, back_right=back_right_abs + front_left=front_left_abs, + back_right=back_right_abs, + deck_extents=self._addressable_areas.deck_extents, + padding_rear=self._addressable_areas.padding_offsets["rear"], + padding_front=self._addressable_areas.padding_offsets["front"], + padding_left_side=self._addressable_areas.padding_offsets["left_side"], + padding_right_side=self._addressable_areas.padding_offsets["right_side"], ) def get_labware_highest_z(self, labware_id: str) -> float: @@ -417,6 +433,16 @@ def get_well_position( offset = offset.copy(update={"z": offset.z + well_depth}) elif well_location.origin == WellOrigin.CENTER: offset = offset.copy(update={"z": offset.z + well_depth / 2.0}) + elif well_location.origin == WellOrigin.MENISCUS: + liquid_height = self._wells.get_last_measured_liquid_height( + labware_id, well_name + ) + if liquid_height is not None: + offset = offset.copy(update={"z": offset.z + liquid_height}) + else: + raise errors.LiquidHeightUnknownError( + "Must liquid probe before specifying WellOrigin.MENISCUS." + ) return Point( x=labware_pos.x + offset.x + well_def.x, @@ -1178,3 +1204,17 @@ def get_offset_location(self, labware_id: str) -> Optional[LabwareOffsetLocation ) return None + + def get_well_volumetric_capacity( + self, labware_id: str, well_id: str + ) -> List[Tuple[float, float]]: + """Return a map of heights to partial volumes.""" + labware_def = self._labware.get_definition(labware_id) + if labware_def.innerLabwareGeometry is None: + raise InvalidWellDefinitionError(message="No InnerLabwareGeometry found.") + well_geometry = labware_def.innerLabwareGeometry.get(well_id) + if well_geometry is None: + raise InvalidWellDefinitionError( + message=f"No InnerWellGeometry found for well id: {well_id}" + ) + return get_well_volumetric_capacity(well_geometry) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 16291062d66..c7f11abb7ec 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -58,8 +58,8 @@ AddLabwareOffsetAction, AddLabwareDefinitionAction, ) -from .abstract_store import HasState, HandlesActions -from .move_types import EdgePathType +from ._abstract_store import HasState, HandlesActions +from ._move_types import EdgePathType # URIs of labware whose definitions accidentally specify an engage height diff --git a/api/src/opentrons/protocol_engine/state/liquids.py b/api/src/opentrons/protocol_engine/state/liquids.py index c19d2fc3b87..9394e4261b1 100644 --- a/api/src/opentrons/protocol_engine/state/liquids.py +++ b/api/src/opentrons/protocol_engine/state/liquids.py @@ -3,7 +3,7 @@ from typing import Dict, List from opentrons.protocol_engine.types import Liquid -from .abstract_store import HasState, HandlesActions +from ._abstract_store import HasState, HandlesActions from ..actions import Action, AddLiquidAction from ..errors import LiquidDoesNotExistError diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 2036032a947..3327020f93e 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -66,7 +66,7 @@ AddModuleAction, AddAbsorbanceReaderLidAction, ) -from .abstract_store import HasState, HandlesActions +from ._abstract_store import HasState, HandlesActions from .module_substates import ( MagneticModuleSubState, HeaterShakerModuleSubState, diff --git a/api/src/opentrons/protocol_engine/state/motion.py b/api/src/opentrons/protocol_engine/state/motion.py index e8eff73447b..d5c9cee53bc 100644 --- a/api/src/opentrons/protocol_engine/state/motion.py +++ b/api/src/opentrons/protocol_engine/state/motion.py @@ -10,7 +10,7 @@ ) from opentrons import motion_planning -from . import move_types +from . import _move_types from .. import errors from ..types import ( MotorAxis, @@ -112,7 +112,7 @@ def get_movement_waypoints_to_well( well_location, ) - move_type = move_types.get_move_type_to_well( + move_type = _move_types.get_move_type_to_well( pipette_id, labware_id, well_name, location, force_direct ) min_travel_z = self._geometry.get_min_travel_z( @@ -326,7 +326,7 @@ def get_touch_tip_waypoints( labware_id, well_name, radius ) - positions = move_types.get_edge_point_list( + positions = _move_types.get_edge_point_list( center_point, x_offset, y_offset, edge_path_type ) critical_point: Optional[CriticalPoint] = None diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 58a798e90bd..0ff0bf847ba 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -1,9 +1,18 @@ """Basic pipette data state and store.""" from __future__ import annotations -from dataclasses import dataclass -from typing import Dict, List, Mapping, Optional, Tuple, Union + +import dataclasses +from typing import ( + Dict, + List, + Mapping, + Optional, + Tuple, + Union, +) from typing_extensions import assert_type +from opentrons_shared_data.errors import EnumeratedError from opentrons_shared_data.pipette import pipette_definition from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control.dev_types import PipetteDict @@ -13,13 +22,9 @@ ) from opentrons.protocol_engine.actions.actions import FailCommandAction from opentrons.protocol_engine.commands.command import DefinedErrorData -from opentrons.protocol_engine.commands.pipetting_common import ( - LiquidNotFoundError, - OverpressureError, - OverpressureErrorInternalData, -) from opentrons.types import MountType, Mount as HwMount, Point +from . import update_types from .. import commands from .. import errors from ..types import ( @@ -41,10 +46,10 @@ SetPipetteMovementSpeedAction, SucceedCommandAction, ) -from .abstract_store import HasState, HandlesActions +from ._abstract_store import HasState, HandlesActions -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class HardwarePipette: """Hardware pipette data.""" @@ -52,7 +57,7 @@ class HardwarePipette: config: PipetteDict -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class CurrentDeckPoint: """The latest deck point and mount the robot has accessed.""" @@ -60,7 +65,7 @@ class CurrentDeckPoint: deck_point: Optional[DeckPoint] -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class BoundingNozzlesOffsets: """Offsets of the bounding nozzles of the pipette.""" @@ -68,7 +73,7 @@ class BoundingNozzlesOffsets: front_right_offset: Point -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class PipetteBoundingBoxOffsets: """Offsets of the corners of the pipette's bounding box.""" @@ -78,7 +83,7 @@ class PipetteBoundingBoxOffsets: front_left_corner: Point -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class StaticPipetteConfig: """Static config for a pipette.""" @@ -100,7 +105,7 @@ class StaticPipetteConfig: lld_settings: Optional[Dict[str, Dict[str, float]]] -@dataclass +@dataclasses.dataclass class PipetteState: """Basic pipette data state and getter methods.""" @@ -279,48 +284,41 @@ def _handle_command( # noqa: C901 def _update_current_location( # noqa: C901 self, action: Union[SucceedCommandAction, FailCommandAction] ) -> None: + if isinstance(action, SucceedCommandAction): + location_update = action.state_update.pipette_location + elif isinstance(action.error, DefinedErrorData): + location_update = action.error.state_update.pipette_location + else: + # The command failed with some undefined error. We have nothing to do. + assert_type(action.error, EnumeratedError) + return + + if location_update != update_types.NO_CHANGE: + match location_update.new_location: + case update_types.Well(labware_id=labware_id, well_name=well_name): + self._state.current_location = CurrentWell( + pipette_id=location_update.pipette_id, + labware_id=labware_id, + well_name=well_name, + ) + case update_types.AddressableArea( + addressable_area_name=addressable_area_name + ): + self._state.current_location = CurrentAddressableArea( + pipette_id=location_update.pipette_id, + addressable_area_name=addressable_area_name, + ) + case None: + self._state.current_location = None + case update_types.NO_CHANGE: + pass + + # todo(mm, 2024-08-29): Port the following isinstance() checks to + # use `state_update`. https://opentrons.atlassian.net/browse/EXEC-639 + # These commands leave the pipette in a new location. # Update current_location to reflect that. if isinstance(action, SucceedCommandAction) and isinstance( - action.command.result, - ( - commands.MoveToWellResult, - commands.PickUpTipResult, - commands.DropTipResult, - commands.AspirateResult, - commands.DispenseResult, - commands.BlowOutResult, - commands.TouchTipResult, - commands.LiquidProbeResult, - commands.TryLiquidProbeResult, - ), - ): - self._state.current_location = CurrentWell( - pipette_id=action.command.params.pipetteId, - labware_id=action.command.params.labwareId, - well_name=action.command.params.wellName, - ) - elif isinstance(action, FailCommandAction) and ( - isinstance(action.error, DefinedErrorData) - and ( - ( - isinstance( - action.running_command, (commands.Aspirate, commands.Dispense) - ) - and isinstance(action.error.public, OverpressureError) - ) - or ( - isinstance(action.running_command, commands.LiquidProbe) - and isinstance(action.error.public, LiquidNotFoundError) - ) - ) - ): - self._state.current_location = CurrentWell( - pipette_id=action.running_command.params.pipetteId, - labware_id=action.running_command.params.labwareId, - well_name=action.running_command.params.wellName, - ) - elif isinstance(action, SucceedCommandAction) and isinstance( action.command.result, ( commands.MoveToAddressableAreaResult, @@ -380,26 +378,40 @@ def _update_current_location( # noqa: C901 ): self._state.current_location = None - def _update_deck_point( + def _update_deck_point( # noqa: C901 self, action: Union[SucceedCommandAction, FailCommandAction] ) -> None: - # This function mostly mirrors self._update_current_location(). + if isinstance(action, SucceedCommandAction): + location_update = action.state_update.pipette_location + elif isinstance(action.error, DefinedErrorData): + location_update = action.error.state_update.pipette_location + else: + # The command failed with some undefined error. We have nothing to do. + assert_type(action.error, EnumeratedError) + return + + if ( + location_update is not update_types.NO_CHANGE + and location_update.new_deck_point is not update_types.NO_CHANGE + ): + loaded_pipette = self._state.pipettes_by_id[location_update.pipette_id] + self._state.current_deck_point = CurrentDeckPoint( + mount=loaded_pipette.mount, deck_point=location_update.new_deck_point + ) + + # todo(mm, 2024-08-29): Port the following isinstance() checks to + # use `state_update`. https://opentrons.atlassian.net/browse/EXEC-639 + # + # These isinstance() checks mostly mirror self._update_current_location(). # See there for explanations. if isinstance(action, SucceedCommandAction) and isinstance( action.command.result, ( - commands.MoveToWellResult, commands.MoveToCoordinatesResult, commands.MoveRelativeResult, commands.MoveToAddressableAreaResult, commands.MoveToAddressableAreaForDropTipResult, - commands.PickUpTipResult, - commands.DropTipResult, - commands.AspirateResult, - commands.DispenseResult, - commands.BlowOutResult, - commands.TouchTipResult, ), ): pipette_id = action.command.params.pipetteId @@ -408,27 +420,6 @@ def _update_deck_point( self._state.current_deck_point = CurrentDeckPoint( mount=loaded_pipette.mount, deck_point=deck_point ) - elif ( - isinstance(action, FailCommandAction) - and isinstance( - action.running_command, - ( - commands.Aspirate, - commands.Dispense, - commands.AspirateInPlace, - commands.DispenseInPlace, - ), - ) - and isinstance(action.error, DefinedErrorData) - and isinstance(action.error.public, OverpressureError) - ): - assert_type(action.error.private, OverpressureErrorInternalData) - pipette_id = action.running_command.params.pipetteId - deck_point = action.error.private.position - loaded_pipette = self._state.pipettes_by_id[pipette_id] - self._state.current_deck_point = CurrentDeckPoint( - mount=loaded_pipette.mount, deck_point=deck_point - ) elif isinstance(action, SucceedCommandAction) and isinstance( action.command.result, @@ -845,8 +836,6 @@ def get_pipette_bounds_at_specified_move_to_position( - primary_nozzle_offset + pipette_bounds_offsets.front_right_corner ) - # TODO (spp, 2024-02-27): remove back right & front left; - # return only back left and front right points. pip_back_right_bound = Point( pip_front_right_bound.x, pip_back_left_bound.y, pip_front_right_bound.z ) diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 4244931efd1..7fc23a8ee2f 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -14,7 +14,7 @@ from ..resources import DeckFixedLabware from ..actions import Action, ActionHandler -from .abstract_store import HasState, HandlesActions +from ._abstract_store import HasState, HandlesActions from .commands import CommandState, CommandStore, CommandView from .addressable_areas import ( AddressableAreaState, @@ -26,6 +26,7 @@ from .modules import ModuleState, ModuleStore, ModuleView from .liquids import LiquidState, LiquidView, LiquidStore from .tips import TipState, TipView, TipStore +from .wells import WellState, WellView, WellStore from .geometry import GeometryView from .motion import MotionView from .config import Config @@ -48,6 +49,7 @@ class State: modules: ModuleState liquids: LiquidState tips: TipState + wells: WellState class StateView(HasState[State]): @@ -61,6 +63,7 @@ class StateView(HasState[State]): _modules: ModuleView _liquid: LiquidView _tips: TipView + _wells: WellView _geometry: GeometryView _motion: MotionView _config: Config @@ -100,6 +103,11 @@ def tips(self) -> TipView: """Get state view selectors for tip state.""" return self._tips + @property + def wells(self) -> WellView: + """Get state view selectors for well state.""" + return self._wells + @property def geometry(self) -> GeometryView: """Get state view selectors for derived geometry state.""" @@ -129,6 +137,7 @@ def get_summary(self) -> StateSummary: completedAt=self._state.commands.run_completed_at, startedAt=self._state.commands.run_started_at, liquids=self._liquid.get_all(), + wells=self._wells.get_all(), hasEverEnteredErrorRecovery=self._commands.get_has_entered_recovery_mode(), ) @@ -196,6 +205,7 @@ def __init__( ) self._liquid_store = LiquidStore() self._tip_store = TipStore() + self._well_store = WellStore() self._substores: List[HandlesActions] = [ self._command_store, @@ -205,6 +215,7 @@ def __init__( self._module_store, self._liquid_store, self._tip_store, + self._well_store, ] self._config = config self._change_notifier = change_notifier or ChangeNotifier() @@ -321,6 +332,7 @@ def _get_next_state(self) -> State: modules=self._module_store.state, liquids=self._liquid_store.state, tips=self._tip_store.state, + wells=self._well_store.state, ) def _initialize_state(self) -> None: @@ -336,11 +348,13 @@ def _initialize_state(self) -> None: self._modules = ModuleView(state.modules) self._liquid = LiquidView(state.liquids) self._tips = TipView(state.tips) + self._wells = WellView(state.wells) # Derived states self._geometry = GeometryView( config=self._config, labware_view=self._labware, + well_view=self._wells, module_view=self._modules, pipette_view=self._pipettes, addressable_area_view=self._addressable_areas, @@ -365,6 +379,7 @@ def _update_state_views(self) -> None: self._modules._state = next_state.modules self._liquid._state = next_state.liquids self._tips._state = next_state.tips + self._wells._state = next_state.wells self._change_notifier.notify() if self._notify_robot_server is not None: self._notify_robot_server() diff --git a/api/src/opentrons/protocol_engine/state/state_summary.py b/api/src/opentrons/protocol_engine/state/state_summary.py index 7e6e003aaa8..66fc4249851 100644 --- a/api/src/opentrons/protocol_engine/state/state_summary.py +++ b/api/src/opentrons/protocol_engine/state/state_summary.py @@ -6,6 +6,7 @@ from ..errors import ErrorOccurrence from ..types import ( EngineStatus, + LiquidHeightSummary, LoadedLabware, LabwareOffset, LoadedModule, @@ -29,3 +30,4 @@ class StateSummary(BaseModel): startedAt: Optional[datetime] completedAt: Optional[datetime] liquids: List[Liquid] = Field(default_factory=list) + wells: List[LiquidHeightSummary] = Field(default_factory=list) diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index 9911b1f85b3..a2c75ba2af4 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -3,7 +3,7 @@ from enum import Enum from typing import Dict, Optional, List, Union -from .abstract_store import HasState, HandlesActions +from ._abstract_store import HasState, HandlesActions from ..actions import ( Action, SucceedCommandAction, diff --git a/api/src/opentrons/protocol_engine/state/update_types.py b/api/src/opentrons/protocol_engine/state/update_types.py new file mode 100644 index 00000000000..a71b1897b42 --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/update_types.py @@ -0,0 +1,120 @@ +"""Structures to represent changes that commands want to make to engine state.""" + + +import dataclasses +import enum +import typing + +from opentrons.protocol_engine.types import DeckPoint + + +class _NoChangeEnum(enum.Enum): + NO_CHANGE = enum.auto() + + +NO_CHANGE: typing.Final = _NoChangeEnum.NO_CHANGE +"""A sentinel value to indicate that a value shouldn't be changed. + +Useful when `None` is semantically unclear or already has some other meaning. +""" + + +NoChangeType: typing.TypeAlias = typing.Literal[_NoChangeEnum.NO_CHANGE] +"""The type of `NO_CHANGE`, as `NoneType` is to `None`. + +Unfortunately, mypy doesn't let us write `Literal[NO_CHANGE]`. Use this instead. +""" + + +@dataclasses.dataclass(frozen=True) +class Well: + """Designates a well in a labware.""" + + labware_id: str + well_name: str + + +@dataclasses.dataclass(frozen=True) +class AddressableArea: + """Designates an addressable area.""" + + addressable_area_name: str + + +@dataclasses.dataclass +class PipetteLocationUpdate: + """Represents an update to perform on a pipette's location.""" + + pipette_id: str + + new_location: Well | AddressableArea | None | NoChangeType + """The pipette's new logical location. + + Note: `new_location=None` means "change the location to `None` (unknown)", + not "do not change the location". + """ + + new_deck_point: DeckPoint | NoChangeType + + +@dataclasses.dataclass +class StateUpdate: + """Represents an update to perform on engine state.""" + + # todo(mm, 2024-08-29): Extend this with something to represent clearing both the + # deck point and the logical location, for e.g. home commands. Consider an explicit + # `CLEAR` sentinel if `None` is confusing. + pipette_location: PipetteLocationUpdate | NoChangeType = NO_CHANGE + + # These convenience functions let the caller avoid the boilerplate of constructing a + # complicated dataclass tree, and they give us a + + @typing.overload + def set_pipette_location( + self, + *, + pipette_id: str, + new_labware_id: str, + new_well_name: str, + new_deck_point: DeckPoint, + ) -> None: + """Schedule a pipette's location to be set to a well.""" + + @typing.overload + def set_pipette_location( + self, + *, + pipette_id: str, + new_addressable_area_name: str, + new_deck_point: DeckPoint, + ) -> None: + """Schedule a pipette's location to be set to an addressable area.""" + pass + + def set_pipette_location( # noqa: D102 + self, + *, + pipette_id: str, + new_labware_id: str | NoChangeType = NO_CHANGE, + new_well_name: str | NoChangeType = NO_CHANGE, + new_addressable_area_name: str | NoChangeType = NO_CHANGE, + new_deck_point: DeckPoint, + ) -> None: + if new_addressable_area_name != NO_CHANGE: + self.pipette_location = PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=AddressableArea( + addressable_area_name=new_addressable_area_name + ), + new_deck_point=new_deck_point, + ) + else: + # These asserts should always pass because of the overloads. + assert new_labware_id != NO_CHANGE + assert new_well_name != NO_CHANGE + + self.pipette_location = PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=Well(labware_id=new_labware_id, well_name=new_well_name), + new_deck_point=new_deck_point, + ) diff --git a/api/src/opentrons/protocol_engine/state/wells.py b/api/src/opentrons/protocol_engine/state/wells.py new file mode 100644 index 00000000000..d74d94a1be0 --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/wells.py @@ -0,0 +1,129 @@ +"""Basic well data state and store.""" +from dataclasses import dataclass +from datetime import datetime +from typing import Dict, List, Optional +from opentrons.protocol_engine.actions.actions import ( + FailCommandAction, + SucceedCommandAction, +) +from opentrons.protocol_engine.commands.liquid_probe import LiquidProbeResult +from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError +from opentrons.protocol_engine.types import LiquidHeightInfo, LiquidHeightSummary + +from ._abstract_store import HasState, HandlesActions +from ..actions import Action +from ..commands import Command + + +@dataclass +class WellState: + """State of all wells.""" + + measured_liquid_heights: Dict[str, Dict[str, LiquidHeightInfo]] + + +class WellStore(HasState[WellState], HandlesActions): + """Well state container.""" + + _state: WellState + + def __init__(self) -> None: + """Initialize a well store and its state.""" + self._state = WellState(measured_liquid_heights={}) + + def handle_action(self, action: Action) -> None: + """Modify state in reaction to an action.""" + if isinstance(action, SucceedCommandAction): + self._handle_succeeded_command(action.command) + if isinstance(action, FailCommandAction): + self._handle_failed_command(action) + + def _handle_succeeded_command(self, command: Command) -> None: + if isinstance(command.result, LiquidProbeResult): + self._set_liquid_height( + labware_id=command.params.labwareId, + well_name=command.params.wellName, + height=command.result.z_position, + time=command.createdAt, + ) + + def _handle_failed_command(self, action: FailCommandAction) -> None: + if isinstance(action.error, LiquidNotFoundError): + self._set_liquid_height( + labware_id=action.error.private.labware_id, + well_name=action.error.private.well_name, + height=None, + time=action.failed_at, + ) + + def _set_liquid_height( + self, labware_id: str, well_name: str, height: float, time: datetime + ) -> None: + """Set the liquid height of the well.""" + lhi = LiquidHeightInfo(height=height, last_measured=time) + if labware_id not in self._state.measured_liquid_heights: + self._state.measured_liquid_heights[labware_id] = {} + self._state.measured_liquid_heights[labware_id][well_name] = lhi + + +class WellView(HasState[WellState]): + """Read-only well state view.""" + + _state: WellState + + def __init__(self, state: WellState) -> None: + """Initialize the computed view of well state. + + Arguments: + state: Well state dataclass used for all calculations. + """ + self._state = state + + def get_all(self) -> List[LiquidHeightSummary]: + """Get all well liquid heights.""" + all_heights: List[LiquidHeightSummary] = [] + for labware, wells in self._state.measured_liquid_heights.items(): + for well, lhi in wells.items(): + lhs = LiquidHeightSummary( + labware_id=labware, + well_name=well, + height=lhi.height, + last_measured=lhi.last_measured, + ) + all_heights.append(lhs) + return all_heights + + def get_all_in_labware(self, labware_id: str) -> List[LiquidHeightSummary]: + """Get all well liquid heights for a particular labware.""" + all_heights: List[LiquidHeightSummary] = [] + for well, lhi in self._state.measured_liquid_heights[labware_id].items(): + lhs = LiquidHeightSummary( + labware_id=labware_id, + well_name=well, + height=lhi.height, + last_measured=lhi.last_measured, + ) + all_heights.append(lhs) + return all_heights + + def get_last_measured_liquid_height( + self, labware_id: str, well_name: str + ) -> Optional[float]: + """Returns the height of the liquid according to the most recent liquid level probe to this well. + + Returns None if no liquid probe has been done. + """ + try: + height = self._state.measured_liquid_heights[labware_id][well_name].height + return height + except KeyError: + return None + + def has_measured_liquid_height(self, labware_id: str, well_name: str) -> bool: + """Returns True if the well has been liquid level probed previously.""" + try: + return bool( + self._state.measured_liquid_heights[labware_id][well_name].height + ) + except KeyError: + return False diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 7a77fdc1512..519d39b6ec7 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -210,6 +210,7 @@ class WellOrigin(str, Enum): TOP = "top" BOTTOM = "bottom" CENTER = "center" + MENISCUS = "meniscus" class DropTipWellOrigin(str, Enum): @@ -311,6 +312,22 @@ class CurrentWell: well_name: str +class LiquidHeightInfo(BaseModel): + """Payload required to store recent measured liquid heights.""" + + height: float + last_measured: datetime + + +class LiquidHeightSummary(BaseModel): + """Payload for liquid state height in StateSummary.""" + + labware_id: str + well_name: str + height: float + last_measured: datetime + + @dataclass(frozen=True) class CurrentAddressableArea: """The latest addressable area the robot has accessed.""" diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index b744c03351c..2e46e64663c 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -34,6 +34,7 @@ ModuleDataProvider, pipette_data_provider, ) +from opentrons.protocol_engine.state.update_types import StateUpdate from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.errors import ErrorCodes, EnumeratedError, PythonException @@ -267,7 +268,9 @@ def map_command( # noqa: C901 ) results.append( pe_actions.SucceedCommandAction( - completed_command, private_result=None + completed_command, + private_result=None, + state_update=StateUpdate(), ) ) @@ -675,6 +678,7 @@ def _map_labware_load( succeed_action = pe_actions.SucceedCommandAction( command=succeeded_command, private_result=None, + state_update=StateUpdate(), ) self._command_count["LOAD_LABWARE"] = count + 1 @@ -741,6 +745,7 @@ def _map_instrument_load( succeed_action = pe_actions.SucceedCommandAction( command=succeeded_command, private_result=pipette_config_result, + state_update=StateUpdate(), ) self._command_count["LOAD_PIPETTE"] = count + 1 @@ -805,8 +810,7 @@ def _map_module_load( started_at=succeeded_command.startedAt, # type: ignore[arg-type] ) succeed_action = pe_actions.SucceedCommandAction( - command=succeeded_command, - private_result=None, + command=succeeded_command, private_result=None, state_update=StateUpdate() ) self._command_count["LOAD_MODULE"] = count + 1 diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 0dc57e0ba1f..9bb5c330788 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -257,18 +257,17 @@ def get_current_command(self) -> Optional[CommandPointer]: return self._protocol_engine.state_view.commands.get_current() def get_command_slice( - self, - cursor: Optional[int], - length: int, + self, cursor: Optional[int], length: int, include_fixit_commands: bool ) -> CommandSlice: """Get a slice of run commands. Args: cursor: Requested index of first command in the returned slice. length: Length of slice to return. + include_fixit_commands: Get all command intents. """ return self._protocol_engine.state_view.commands.get_slice( - cursor=cursor, length=length + cursor=cursor, length=length, include_fixit_commands=include_fixit_commands ) def get_command_error_slice( diff --git a/api/src/opentrons/protocols/api_support/constants.py b/api/src/opentrons/protocols/api_support/constants.py index b350d970055..8da286acb62 100644 --- a/api/src/opentrons/protocols/api_support/constants.py +++ b/api/src/opentrons/protocols/api_support/constants.py @@ -4,5 +4,5 @@ OPENTRONS_NAMESPACE = "opentrons" CUSTOM_NAMESPACE = "custom_beta" -STANDARD_DEFS_PATH = Path("labware/definitions/2") +STANDARD_DEFS_PATH = Path("labware/definitions") USER_DEFS_PATH = get_opentrons_path("labware_user_definitions_dir_v2") diff --git a/api/src/opentrons/protocols/api_support/definitions.py b/api/src/opentrons/protocols/api_support/definitions.py index 799af1993f3..ad692e03828 100644 --- a/api/src/opentrons/protocols/api_support/definitions.py +++ b/api/src/opentrons/protocols/api_support/definitions.py @@ -1,6 +1,6 @@ from .types import APIVersion -MAX_SUPPORTED_VERSION = APIVersion(2, 20) +MAX_SUPPORTED_VERSION = APIVersion(2, 21) """The maximum supported protocol API version in this release.""" MIN_SUPPORTED_VERSION = APIVersion(2, 0) diff --git a/api/src/opentrons/protocols/api_support/util.py b/api/src/opentrons/protocols/api_support/util.py index 3438692de2f..da4ceff7360 100644 --- a/api/src/opentrons/protocols/api_support/util.py +++ b/api/src/opentrons/protocols/api_support/util.py @@ -391,13 +391,3 @@ def _check_version_wrapper(*args: Any, **kwargs: Any) -> Any: return cast(FuncT, _check_version_wrapper) return _set_version - - -class ModifiedList(list[str]): - def __contains__(self, item: object) -> bool: - if not isinstance(item, str): - return False - for name in self: - if name == item.replace("-", "_").lower(): - return True - return False diff --git a/api/src/opentrons/protocols/labware.py b/api/src/opentrons/protocols/labware.py index 02f617fd72c..ed1b7d15219 100644 --- a/api/src/opentrons/protocols/labware.py +++ b/api/src/opentrons/protocols/labware.py @@ -2,14 +2,12 @@ import logging import json -import os from pathlib import Path -from typing import Any, AnyStr, List, Dict, Optional, Union +from typing import Any, AnyStr, Dict, Optional, Union import jsonschema # type: ignore -from opentrons.protocols.api_support.util import ModifiedList from opentrons_shared_data import load_shared_data, get_shared_data_root from opentrons.protocols.api_support.constants import ( OPENTRONS_NAMESPACE, @@ -63,29 +61,6 @@ def get_labware_definition( return _get_standard_labware_definition(load_name, namespace, version) -def get_all_labware_definitions() -> List[str]: - """ - Return a list of standard and custom labware definitions with load_name + - name_space + version existing on the robot - """ - labware_list = ModifiedList() - - def _check_for_subdirectories(path: Union[str, Path, os.DirEntry[str]]) -> None: - with os.scandir(path) as top_path: - for sub_dir in top_path: - if sub_dir.is_dir(): - labware_list.append(sub_dir.name) - - # check for standard labware - _check_for_subdirectories(get_shared_data_root() / STANDARD_DEFS_PATH) - - # check for custom labware - for namespace in os.scandir(USER_DEFS_PATH): - _check_for_subdirectories(namespace) - - return labware_list - - def save_definition( labware_def: LabwareDefinition, force: bool = False, location: Optional[Path] = None ) -> None: @@ -114,7 +89,6 @@ def save_definition( f'Saving definitions to the "{OPENTRONS_NAMESPACE}" namespace ' + "is not permitted" ) - def_path = _get_path_to_labware(load_name, namespace, version, location) if not force and def_path.is_file(): @@ -219,7 +193,6 @@ def _get_standard_labware_definition( Definitions Folder from the Opentrons App before uploading your protocol. """ - if namespace is None: for fallback_namespace in [OPENTRONS_NAMESPACE, CUSTOM_NAMESPACE]: try: @@ -252,9 +225,21 @@ def _get_path_to_labware( ) -> Path: if namespace == OPENTRONS_NAMESPACE: # all labware in OPENTRONS_NAMESPACE is stored in shared data - return ( - get_shared_data_root() / STANDARD_DEFS_PATH / load_name / f"{version}.json" + schema_3_path = ( + get_shared_data_root() + / STANDARD_DEFS_PATH + / "3" + / load_name + / f"{version}.json" + ) + schema_2_path = ( + get_shared_data_root() + / STANDARD_DEFS_PATH + / "2" + / load_name + / f"{version}.json" ) + return schema_3_path if schema_3_path.exists() else schema_2_path if not base_path: base_path = USER_DEFS_PATH def_path = base_path / namespace / load_name / f"{version}.json" diff --git a/api/src/opentrons/protocols/parameters/csv_parameter_interface.py b/api/src/opentrons/protocols/parameters/csv_parameter_interface.py index 20627322547..6da9a0f7aaf 100644 --- a/api/src/opentrons/protocols/parameters/csv_parameter_interface.py +++ b/api/src/opentrons/protocols/parameters/csv_parameter_interface.py @@ -16,7 +16,10 @@ def __init__(self, contents: Optional[bytes], api_version: APIVersion) -> None: @property def file(self) -> TextIO: - """Returns the file handler for the CSV file.""" + """Returns the file handler for the CSV file. + + The file is treated as read-only, UTF-8-encoded text. + """ if self._file is None: text = self.contents temporary_file = NamedTemporaryFile("r+") @@ -30,7 +33,7 @@ def file(self) -> TextIO: @property def file_opened(self) -> bool: - """Return if a file handler has been opened for the CSV parameter.""" + """Returns ``True`` if a file handler is open for the CSV parameter.""" return self._file is not None @property @@ -45,10 +48,22 @@ def contents(self) -> str: def parse_as_csv( self, detect_dialect: bool = True, **kwargs: Any ) -> List[List[str]]: - """Returns a list of rows with each row represented as a list of column elements. + """Parses the CSV data and returns a list of lists. + + Each item in the parent list corresponds to a row in the CSV file. + If the CSV has a header, that will be the first row in the list: ``.parse_as_csv()[0]``. - If there is a header for the CSV that will be the first row in the list (i.e. `.rows()[0]`). - All elements will be represented as strings, even if they are numeric in nature. + Each item in the child lists corresponds to a single cell within its row. + The data for each cell is represented as a string. You may need to trim whitespace + or otherwise validate string contents before passing them as inputs to other API methods. + For numeric data, cast these strings to integers or floating point numbers, + as appropriate. + + :param detect_dialect: If ``True``, examine the file and try to assign it a + :py:class:`csv.Dialect` to improve parsing behavior. + :param kwargs: For advanced CSV handling, you can pass any of the + `formatting parameters `_ + accepted by :py:func:`csv.reader` from the Python standard library. """ rows: List[List[str]] = [] if detect_dialect: @@ -69,4 +84,11 @@ def parse_as_csv( rows.append(row) except (UnicodeDecodeError, csv.Error): raise ParameterValueError("Cannot parse provided CSV contents.") + return self._remove_trailing_empty_rows(rows) + + @staticmethod + def _remove_trailing_empty_rows(rows: List[List[str]]) -> List[List[str]]: + """Removes any trailing empty rows.""" + while rows and rows[-1] == []: + rows.pop() return rows diff --git a/api/tests/opentrons/config/ot3_settings.py b/api/tests/opentrons/config/ot3_settings.py index 3d46d22d0b0..38353c05a3c 100644 --- a/api/tests/opentrons/config/ot3_settings.py +++ b/api/tests/opentrons/config/ot3_settings.py @@ -125,6 +125,7 @@ "output_option": OutputOptions.stream_to_csv, "aspirate_while_sensing": False, "z_overlap_between_passes_mm": 0.1, + "plunger_reset_offset": 2.0, "samples_for_baselining": 20, "sample_time_sec": 0.004, "data_files": {"PRIMARY": "/data/pressure_sensor_data.csv"}, diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index 137b0b91a00..ac25d19a3e2 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -183,6 +183,7 @@ def fake_liquid_settings() -> LiquidProbeSettings: output_option=OutputOptions.can_bus_only, aspirate_while_sensing=False, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files={InstrumentProbeType.PRIMARY: "fake_file_name"}, @@ -723,6 +724,7 @@ async def test_liquid_probe( plunger_speed=fake_liquid_settings.plunger_speed, threshold_pascals=fake_liquid_settings.sensor_threshold_pascals, plunger_impulse_time=fake_liquid_settings.plunger_impulse_time, + num_baseline_reads=fake_liquid_settings.samples_for_baselining, output_option=fake_liquid_settings.output_option, ) except PipetteLiquidNotFoundError: diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index 68f5e01b2dd..08ecb3afa43 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -117,12 +117,13 @@ def fake_settings() -> CapacitivePassSettings: def fake_liquid_settings() -> LiquidProbeSettings: return LiquidProbeSettings( mount_speed=5, - plunger_speed=20, + plunger_speed=15, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.can_bus_only, aspirate_while_sensing=False, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files={InstrumentProbeType.PRIMARY: "fake_file_name"}, @@ -823,26 +824,42 @@ async def test_liquid_probe( mock_liquid_probe.return_value = 140 fake_settings_aspirate = LiquidProbeSettings( mount_speed=5, - plunger_speed=20, + plunger_speed=15, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.can_bus_only, aspirate_while_sensing=True, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files={InstrumentProbeType.PRIMARY: "fake_file_name"}, ) fake_max_z_dist = 10.0 + non_responsive_z_mm = ot3_hardware.liquid_probe_non_responsive_z_distance( + fake_settings_aspirate.mount_speed, + fake_settings_aspirate.samples_for_baselining, + fake_settings_aspirate.sample_time_sec, + ) + + probe_pass_overlap = 0.1 + probe_pass_z_offset_mm = non_responsive_z_mm + probe_pass_overlap + probe_safe_reset_mm = max(2.0, probe_pass_z_offset_mm) + await ot3_hardware.liquid_probe(mount, fake_max_z_dist, fake_settings_aspirate) mock_move_to_plunger_bottom.call_count == 2 mock_liquid_probe.assert_called_once_with( mount, - 46, + ( + (fake_max_z_dist - probe_pass_z_offset_mm + probe_safe_reset_mm) + / fake_settings_aspirate.mount_speed + ) + * fake_settings_aspirate.plunger_speed, fake_settings_aspirate.mount_speed, (fake_settings_aspirate.plunger_speed * -1), fake_settings_aspirate.sensor_threshold_pascals, fake_settings_aspirate.plunger_impulse_time, + fake_settings_aspirate.samples_for_baselining, fake_settings_aspirate.output_option, fake_settings_aspirate.data_files, probe=InstrumentProbeType.PRIMARY, @@ -900,7 +917,6 @@ async def test_liquid_probe_plunger_moves( PipetteLiquidNotFoundError, PipetteLiquidNotFoundError, PipetteLiquidNotFoundError, - PipetteLiquidNotFoundError, 140, ] @@ -919,17 +935,6 @@ async def test_liquid_probe_plunger_moves( probe_pass_z_offset_mm = non_responsive_z_mm + probe_pass_overlap probe_safe_reset_mm = max(2.0, probe_pass_z_offset_mm) - # simulate multiple passes of liquid probe - mock_gantry_position.side_effect = [ - Point(x=0, y=0, z=100), - Point(x=0, y=0, z=100), - Point(x=0, y=0, z=100), - Point(x=0, y=0, z=82.15), - Point(x=0, y=0, z=64.3), - Point(x=0, y=0, z=46.45), - Point(x=0, y=0, z=28.6), - Point(x=0, y=0, z=25), - ] probe_start_pos = await ot3_hardware.gantry_position(mount) safe_plunger_pos = Point( probe_start_pos.x, @@ -940,7 +945,20 @@ async def test_liquid_probe_plunger_moves( p_impulse_mm = config.plunger_impulse_time * config.plunger_speed p_total_mm = pipette.plunger_positions.bottom - pipette.plunger_positions.top p_working_mm = p_total_mm - (pipette.backlash_distance + p_impulse_mm) - + # simulate multiple passes of liquid probe + z_pass = ( + (p_total_mm - pipette.backlash_distance) + / config.plunger_speed + * config.mount_speed + ) + mock_gantry_position.side_effect = [ + Point(x=0, y=0, z=100), + Point(x=0, y=0, z=100), + Point(x=0, y=0, z=100 - z_pass), + Point(x=0, y=0, z=100 - 2 * z_pass), + Point(x=0, y=0, z=100 - 3 * z_pass), + Point(x=0, y=0, z=25), + ] max_z_time = ( fake_max_z_dist - (probe_start_pos.z - safe_plunger_pos.z) ) / config.mount_speed @@ -1083,6 +1101,7 @@ async def test_multi_liquid_probe( output_option=OutputOptions.can_bus_only, aspirate_while_sensing=True, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files={InstrumentProbeType.PRIMARY: "fake_file_name"}, @@ -1099,6 +1118,7 @@ async def test_multi_liquid_probe( (fake_settings_aspirate.plunger_speed * -1), fake_settings_aspirate.sensor_threshold_pascals, fake_settings_aspirate.plunger_impulse_time, + fake_settings_aspirate.samples_for_baselining, fake_settings_aspirate.output_option, fake_settings_aspirate.data_files, probe=InstrumentProbeType.PRIMARY, @@ -1134,6 +1154,7 @@ async def _fake_pos_update_and_raise( plunger_speed: float, threshold_pascals: float, plunger_impulse_time: float, + num_baseline_reads: int, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, @@ -1158,6 +1179,7 @@ async def _fake_pos_update_and_raise( output_option=OutputOptions.can_bus_only, aspirate_while_sensing=True, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files={InstrumentProbeType.PRIMARY: "fake_file_name"}, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py index d0171bff798..147368e0734 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py @@ -510,6 +510,11 @@ def test_deck_conflict_raises_for_bad_pipette_move( MountType.LEFT: Point(463.7, 433.3, 0.0), MountType.RIGHT: Point(517.7, 433.3), }, + deck_extents=Point(477.2, 493.8, 0.0), + padding_rear=-181.21, + padding_front=55.8, + padding_left_side=31.88, + padding_right_side=-80.32, ) ) decoy.when( @@ -677,6 +682,11 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( MountType.LEFT: Point(463.7, 433.3, 0.0), MountType.RIGHT: Point(517.7, 433.3), }, + deck_extents=Point(477.2, 493.8, 0.0), + padding_rear=-181.21, + padding_front=55.8, + padding_left_side=31.88, + padding_right_side=-80.32, ) ) @@ -696,7 +706,7 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( ) with pytest.raises( deck_conflict.PartialTipMovementNotAllowedError, - match="collision with thermocycler lid in deck slot A1.", + match="Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", ): deck_conflict.check_safe_for_pipette_movement( engine_state=mock_state_view, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py index 31b562f7e81..96efbbdde8d 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py @@ -149,6 +149,23 @@ def test_get_center( assert subject.get_center() == Point(1, 2, 3) +def test_get_meniscus( + decoy: Decoy, mock_engine_client: EngineClient, subject: WellCore +) -> None: + """It should get a well bottom.""" + decoy.when( + mock_engine_client.state.geometry.get_well_position( + labware_id="labware-id", + well_name="well-name", + well_location=WellLocation( + origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=2.5) + ), + ) + ).then_return(Point(1, 2, 3)) + + assert subject.get_meniscus(z_offset=2.5) == Point(1, 2, 3) + + def test_has_tip( decoy: Decoy, mock_engine_client: EngineClient, subject: WellCore ) -> None: diff --git a/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py b/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py index 95cb07aa2cf..1cf0bb360fb 100644 --- a/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py +++ b/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py @@ -15,7 +15,7 @@ LabwareOffsetLocation, ModuleModel, ) -from opentrons.protocol_engine.state import LabwareView +from opentrons.protocol_engine.state.labware import LabwareView from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.legacy.labware_offset_provider import ( diff --git a/api/tests/opentrons/protocol_api/test_well.py b/api/tests/opentrons/protocol_api/test_well.py index 00cbbac8fa7..3a2ba81b9fa 100644 --- a/api/tests/opentrons/protocol_api/test_well.py +++ b/api/tests/opentrons/protocol_api/test_well.py @@ -101,6 +101,17 @@ def test_well_center(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> N assert result.labware.as_well() is subject +def test_well_meniscus(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: + """It should get a Location representing the meniscus of the well.""" + decoy.when(mock_well_core.get_meniscus(z_offset=4.2)).then_return(Point(1, 2, 3)) + + result = subject.meniscus(4.2) + + assert isinstance(result, Location) + assert result.point == Point(1, 2, 3) + assert result.labware.as_well() is subject + + def test_has_tip(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: """It should get tip state from the core.""" decoy.when(mock_well_core.has_tip()).then_return(True) diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py index 59523fd2c91..1d3388d3d97 100644 --- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py +++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py @@ -61,13 +61,6 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: ): instrument.pick_up_tip(badly_placed_tiprack.wells_by_name()["A1"]) - with pytest.raises( - PartialTipMovementNotAllowedError, match="outside of robot bounds" - ): - # Picking up from A1 in an east-most slot using a configuration with column 12 would - # result in a collision with the side of the robot. - instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A1"]) - instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A12"]) instrument.aspirate(50, well_placed_labware.wells_by_name()["A4"]) diff --git a/api/tests/opentrons/protocol_engine/commands/calibration/test_move_to_maintenance_position.py b/api/tests/opentrons/protocol_engine/commands/calibration/test_move_to_maintenance_position.py index dd057d1cf8a..ca8bb9de5bd 100644 --- a/api/tests/opentrons/protocol_engine/commands/calibration/test_move_to_maintenance_position.py +++ b/api/tests/opentrons/protocol_engine/commands/calibration/test_move_to_maintenance_position.py @@ -14,7 +14,7 @@ ) from opentrons.protocol_engine.commands.command import SuccessData -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.types import MountType, Mount, Point from opentrons.hardware_control.types import Axis, CriticalPoint @@ -35,7 +35,7 @@ def subject( @pytest.mark.ot3_only @pytest.mark.parametrize("mount_type", [MountType.LEFT, MountType.RIGHT]) -async def test_calibration_move_to_location_implementatio_for_attach_instrument( +async def test_calibration_move_to_location_implementation_for_attach_instrument( decoy: Decoy, subject: MoveToMaintenancePositionImplementation, state_view: StateView, @@ -79,7 +79,7 @@ async def test_calibration_move_to_location_implementatio_for_attach_instrument( @pytest.mark.ot3_only @pytest.mark.parametrize("mount_type", [MountType.LEFT, MountType.RIGHT]) -async def test_calibration_move_to_location_implementatio_for_attach_plate( +async def test_calibration_move_to_location_implementation_for_attach_plate( decoy: Decoy, subject: MoveToMaintenancePositionImplementation, state_view: StateView, @@ -113,11 +113,18 @@ async def test_calibration_move_to_location_implementatio_for_attach_plate( await ot3_hardware_api.move_axes( position={ Axis.Z_L: 90, + } + ), + await ot3_hardware_api.disengage_axes( + [Axis.Z_L], + ), + await ot3_hardware_api.move_axes( + position={ Axis.Z_R: 105, } ), await ot3_hardware_api.disengage_axes( - [Axis.Z_L, Axis.Z_R], + [Axis.Z_R], ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/conftest.py b/api/tests/opentrons/protocol_engine/commands/conftest.py index 8749023c96f..1d27dea0536 100644 --- a/api/tests/opentrons/protocol_engine/commands/conftest.py +++ b/api/tests/opentrons/protocol_engine/commands/conftest.py @@ -16,7 +16,7 @@ GantryMover, ) from opentrons.protocol_engine.resources.model_utils import ModelUtils -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView @pytest.fixture diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_close_labware_latch.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_close_labware_latch.py index d728b97cb4d..fbd1fadcc23 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_close_labware_latch.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_close_labware_latch.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import HeaterShaker -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, HeaterShakerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_heater.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_heater.py index 0da296f71d6..5e8a65a06e8 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_heater.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_heater.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import HeaterShaker -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, HeaterShakerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_shaker.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_shaker.py index 3ab339f97e7..db5e1aba138 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_shaker.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_shaker.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import HeaterShaker -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, HeaterShakerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_open_labware_latch.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_open_labware_latch.py index 6894c1d7e80..3a834d7a410 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_open_labware_latch.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_open_labware_latch.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import HeaterShaker -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, HeaterShakerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_and_wait_for_shake_speed.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_and_wait_for_shake_speed.py index 85e92ffd5b0..6d2b6cae716 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_and_wait_for_shake_speed.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_and_wait_for_shake_speed.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import HeaterShaker -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, HeaterShakerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_target_temperature.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_target_temperature.py index b220c15ebef..51df5f560b3 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_target_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_target_temperature.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import HeaterShaker -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, HeaterShakerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_wait_for_temperature.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_wait_for_temperature.py index a575e8d4795..c256a480f16 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_wait_for_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_wait_for_temperature.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import HeaterShaker -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, HeaterShakerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_disengage.py b/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_disengage.py index b87cd5d3f3b..e1103518178 100644 --- a/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_disengage.py +++ b/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_disengage.py @@ -4,7 +4,7 @@ from opentrons.hardware_control.modules import MagDeck from opentrons.protocol_engine.execution import EquipmentHandler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( MagneticModuleSubState, MagneticModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_engage.py b/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_engage.py index 6563371345e..5feddee3e2e 100644 --- a/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_engage.py +++ b/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_engage.py @@ -3,7 +3,7 @@ from decoy import Decoy from opentrons.hardware_control.modules import MagDeck -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( MagneticModuleId, MagneticModuleSubState, diff --git a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_deactivate.py b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_deactivate.py index 7e73ec94dc6..dfe821c6bbb 100644 --- a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_deactivate.py +++ b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_deactivate.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import TempDeck -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( TemperatureModuleSubState, TemperatureModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_set_target_temperature.py b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_set_target_temperature.py index cd57f86a4c6..0af71263e96 100644 --- a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_set_target_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_set_target_temperature.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import TempDeck -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( TemperatureModuleSubState, TemperatureModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_wait_for_temperature.py b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_wait_for_temperature.py index df18e8a144c..fb9456321b9 100644 --- a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_wait_for_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_wait_for_temperature.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import TempDeck -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( TemperatureModuleSubState, TemperatureModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index b1e3c1e52df..779242ccb84 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -5,10 +5,8 @@ from decoy import matchers, Decoy import pytest -from opentrons.protocol_engine.commands.pipetting_common import ( - OverpressureError, - OverpressureErrorInternalData, -) +from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.state import update_types from opentrons.types import MountType, Point from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint @@ -19,7 +17,7 @@ ) from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import ( MovementHandler, @@ -98,6 +96,13 @@ async def test_aspirate_implementation_no_prep( assert result == SuccessData( public=AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)), private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well(labware_id="123", well_name="A3"), + new_deck_point=DeckPoint(x=1, y=2, z=3), + ) + ), ) @@ -157,6 +162,13 @@ async def test_aspirate_implementation_with_prep( assert result == SuccessData( public=AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)), private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well(labware_id="123", well_name="A3"), + new_deck_point=DeckPoint(x=1, y=2, z=3), + ) + ), ) decoy.verify( @@ -173,6 +185,7 @@ async def test_aspirate_implementation_with_prep( async def test_aspirate_raises_volume_error( decoy: Decoy, pipetting: PipettingHandler, + movement: MovementHandler, mock_command_note_adder: CommandNoteAdder, subject: AspirateImplementation, ) -> None: @@ -190,6 +203,16 @@ async def test_aspirate_raises_volume_error( decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) + decoy.when( + await movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=location, + current_well=None, + ), + ).then_return(Point(1, 2, 3)) + decoy.when( await pipetting.aspirate_in_place( pipette_id="abc", @@ -268,7 +291,13 @@ async def test_overpressure_error( wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=position.x, y=position.y, z=position.z) + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=update_types.Well( + labware_id=labware_id, well_name=well_name + ), + new_deck_point=DeckPoint(x=position.x, y=position.y, z=position.z), + ) ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py index 26f62231a56..3891dd90294 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py @@ -19,14 +19,8 @@ from opentrons.protocol_engine.errors.exceptions import PipetteNotReadyToAspirateError from opentrons.protocol_engine.notes import CommandNoteAdder from opentrons.protocol_engine.resources import ModelUtils -from opentrons.protocol_engine.state import ( - StateStore, -) -from opentrons.protocol_engine.types import DeckPoint -from opentrons.protocol_engine.commands.pipetting_common import ( - OverpressureError, - OverpressureErrorInternalData, -) +from opentrons.protocol_engine.state.state import StateStore +from opentrons.protocol_engine.commands.pipetting_common import OverpressureError @pytest.fixture @@ -207,7 +201,4 @@ async def test_overpressure_error( wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=position.x, y=position.y, z=position.z) - ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py index 919d37e9a76..d762d18096e 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py @@ -3,7 +3,8 @@ from opentrons.types import Point from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.commands import ( BlowOutResult, BlowOutImplementation, @@ -54,7 +55,18 @@ async def test_blow_out_implementation( result = await subject.execute(data) assert result == SuccessData( - public=BlowOutResult(position=DeckPoint(x=1, y=2, z=3)), private=None + public=BlowOutResult(position=DeckPoint(x=1, y=2, z=3)), + private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="labware-id", + well_name="C6", + ), + new_deck_point=DeckPoint(x=1, y=2, z=3), + ) + ), ) decoy.verify( diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py index a14bcdc8019..983decaa092 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py @@ -1,7 +1,7 @@ """Test blow-out-in-place commands.""" from decoy import Decoy -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.commands.blow_out_in_place import ( BlowOutInPlaceParams, BlowOutInPlaceResult, diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 86c4f6ac93b..223cfcc78c9 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -8,6 +8,7 @@ from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint from opentrons.protocol_engine.execution import MovementHandler, PipettingHandler +from opentrons.protocol_engine.state import update_types from opentrons.types import Point from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData @@ -17,10 +18,7 @@ DispenseImplementation, ) from opentrons.protocol_engine.resources.model_utils import ModelUtils -from opentrons.protocol_engine.commands.pipetting_common import ( - OverpressureError, - OverpressureErrorInternalData, -) +from opentrons.protocol_engine.commands.pipetting_common import OverpressureError @pytest.fixture @@ -75,6 +73,16 @@ async def test_dispense_implementation( assert result == SuccessData( public=DispenseResult(volume=42, position=DeckPoint(x=1, y=2, z=3)), private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id-abc123", + new_location=update_types.Well( + labware_id="labware-id-abc123", + well_name="A3", + ), + new_deck_point=DeckPoint.construct(x=1, y=2, z=3), + ), + ), ) @@ -134,7 +142,14 @@ async def test_overpressure_error( wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=position.x, y=position.y, z=position.z) + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="labware-id", + well_name="well-name", + ), + new_deck_point=DeckPoint.construct(x=1, y=2, z=3), + ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py index 3b37e1078b7..53a491ad211 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py @@ -14,11 +14,7 @@ DispenseInPlaceResult, DispenseInPlaceImplementation, ) -from opentrons.protocol_engine.types import DeckPoint -from opentrons.protocol_engine.commands.pipetting_common import ( - OverpressureError, - OverpressureErrorInternalData, -) +from opentrons.protocol_engine.commands.pipetting_common import OverpressureError from opentrons.protocol_engine.resources import ModelUtils @@ -97,7 +93,4 @@ async def test_overpressure_error( wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=position.x, y=position.y, z=position.z) - ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 9690dcc2461..d0d69eeccfa 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -9,7 +9,8 @@ WellOffset, DeckPoint, ) -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import MovementHandler, TipHandler from opentrons.types import Point @@ -112,7 +113,18 @@ async def test_drop_tip_implementation( result = await subject.execute(params) assert result == SuccessData( - public=DropTipResult(position=DeckPoint(x=111, y=222, z=333)), private=None + public=DropTipResult(position=DeckPoint(x=111, y=222, z=333)), + private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well( + labware_id="123", + well_name="A3", + ), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ) + ), ) decoy.verify( @@ -174,5 +186,16 @@ async def test_drop_tip_with_alternating_locations( result = await subject.execute(params) assert result == SuccessData( - public=DropTipResult(position=DeckPoint(x=111, y=222, z=333)), private=None + public=DropTipResult(position=DeckPoint(x=111, y=222, z=333)), + private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well( + labware_id="123", + well_name="A3", + ), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index 61f4339360d..bd28916239b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -13,10 +13,8 @@ from decoy import matchers, Decoy import pytest -from opentrons.protocol_engine.commands.pipetting_common import ( - LiquidNotFoundError, - LiquidNotFoundErrorInternalData, -) +from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError +from opentrons.protocol_engine.state import update_types from opentrons.types import MountType, Point from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint @@ -30,14 +28,12 @@ ) from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData -from opentrons.protocol_engine.state import StateView from opentrons.protocol_engine.execution import ( MovementHandler, PipettingHandler, ) from opentrons.protocol_engine.resources.model_utils import ModelUtils -from opentrons.protocol_engine.types import LoadedPipette EitherImplementationType = Union[ @@ -96,7 +92,7 @@ def subject( ) -async def test_liquid_probe_implementation_no_prep( +async def test_liquid_probe_implementation( decoy: Decoy, movement: MovementHandler, pipetting: PipettingHandler, @@ -104,7 +100,7 @@ async def test_liquid_probe_implementation_no_prep( params_type: EitherParamsType, result_type: EitherResultType, ) -> None: - """A Liquid Probe should have an execution implementation without preparing to aspirate.""" + """It should move to the destination and do a liquid probe there.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) data = params_type( @@ -140,66 +136,12 @@ async def test_liquid_probe_implementation_no_prep( assert result == SuccessData( public=result_type(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), private=None, - ) - - -async def test_liquid_probe_implementation_with_prep( - decoy: Decoy, - state_view: StateView, - movement: MovementHandler, - pipetting: PipettingHandler, - subject: EitherImplementation, - params_type: EitherParamsType, - result_type: EitherResultType, -) -> None: - """A Liquid Probe should have an execution implementation with preparing to aspirate.""" - location = WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2)) - - data = params_type( - pipetteId="abc", - labwareId="123", - wellName="A3", - wellLocation=location, - ) - - decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(False) - - decoy.when(state_view.pipettes.get(pipette_id="abc")).then_return( - LoadedPipette.construct( # type:ignore[call-arg] - mount=MountType.LEFT - ) - ) - decoy.when( - await movement.move_to_well( - pipette_id="abc", labware_id="123", well_name="A3", well_location=location - ), - ).then_return(Point(x=1, y=2, z=3)) - - decoy.when( - await pipetting.liquid_probe_in_place( - pipette_id="abc", - labware_id="123", - well_name="A3", - well_location=location, - ), - ).then_return(15.0) - - result = await subject.execute(data) - - assert type(result.public) is result_type # Pydantic v1 only compares the fields. - assert result == SuccessData( - public=result_type(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), - private=None, - ) - - decoy.verify( - await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", - well_location=WellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2) - ), + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well(labware_id="123", well_name="A3"), + new_deck_point=DeckPoint(x=1, y=2, z=3), + ) ), ) @@ -259,6 +201,13 @@ async def test_liquid_not_found_error( result = await subject.execute(data) + expected_state_update = update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=update_types.Well(labware_id=labware_id, well_name=well_name), + new_deck_point=DeckPoint(x=position.x, y=position.y, z=position.z), + ) + ) if isinstance(subject, LiquidProbeImplementation): assert result == DefinedErrorData( public=LiquidNotFoundError.construct( @@ -266,9 +215,7 @@ async def test_liquid_not_found_error( createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], ), - private=LiquidNotFoundErrorInternalData( - position=DeckPoint(x=position.x, y=position.y, z=position.z) - ), + state_update=expected_state_update, ) else: assert result == SuccessData( @@ -277,6 +224,7 @@ async def test_liquid_not_found_error( position=DeckPoint(x=position.x, y=position.y, z=position.z), ), private=None, + state_update=expected_state_update, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py index 867e8555386..c9bf96c12c2 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py @@ -18,7 +18,7 @@ ) from opentrons.protocol_engine.execution import LoadedLabwareData, EquipmentHandler from opentrons.protocol_engine.resources import labware_validation -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.load_labware import ( @@ -32,7 +32,7 @@ def patch_mock_labware_validation( decoy: Decoy, monkeypatch: pytest.MonkeyPatch ) -> None: - """Mock out move_types.py functions.""" + """Mock out labware_validations.py functions.""" for name, func in inspect.getmembers(labware_validation, inspect.isfunction): monkeypatch.setattr(labware_validation, name, decoy.mock(func=func)) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py b/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py index f1f998b85e7..3ccaaea15d0 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py @@ -8,7 +8,7 @@ LoadLiquidImplementation, LoadLiquidParams, ) -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView @pytest.fixture diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_module.py b/api/tests/opentrons/protocol_engine/commands/test_load_module.py index 2dbd0e31e97..9479a724110 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_module.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_module.py @@ -4,7 +4,7 @@ from decoy import Decoy from opentrons.protocol_engine.errors import LocationIsOccupiedError -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons_shared_data.robot.types import RobotType from opentrons.types import DeckSlotName from opentrons.protocol_engine.types import ( diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py index 72721343478..6b7bd77c32c 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py @@ -12,7 +12,7 @@ from opentrons.protocol_engine.resources.pipette_data_provider import ( LoadedStaticPipetteData, ) -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.load_pipette import ( LoadPipetteParams, diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py index 0872525faf0..f523a8bd2d9 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py @@ -21,7 +21,7 @@ DeckType, AddressableAreaLocation, ) -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.move_labware import ( MoveLabwareParams, @@ -39,7 +39,7 @@ def patch_mock_labware_validation( decoy: Decoy, monkeypatch: pytest.MonkeyPatch ) -> None: - """Mock out move_types.py functions.""" + """Mock out labware_validation.py functions.""" for name, func in inspect.getmembers(labware_validation, inspect.isfunction): monkeypatch.setattr(labware_validation, name, decoy.mock(func=func)) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py index 20d944b6f87..95d80d86590 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py @@ -3,7 +3,7 @@ from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector from opentrons.protocol_engine.execution import MovementHandler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point from opentrons.protocol_engine.commands.command import SuccessData diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py index 5576b662566..bea5ebc6bca 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py @@ -3,7 +3,7 @@ from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector from opentrons.protocol_engine.execution import MovementHandler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point from opentrons.protocol_engine.commands.command import SuccessData diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py index c630c913480..6038ea9ef6e 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py @@ -3,7 +3,7 @@ from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.execution import MovementHandler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import DeckPoint from opentrons.types import Point diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py index ddd6cf51a21..d91822979f2 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py @@ -3,6 +3,7 @@ from opentrons.protocol_engine import WellLocation, WellOffset, DeckPoint from opentrons.protocol_engine.execution import MovementHandler +from opentrons.protocol_engine.state import update_types from opentrons.types import Point from opentrons.protocol_engine.commands.command import SuccessData @@ -45,5 +46,13 @@ async def test_move_to_well_implementation( result = await subject.execute(data) assert result == SuccessData( - public=MoveToWellResult(position=DeckPoint(x=9, y=8, z=7)), private=None + public=MoveToWellResult(position=DeckPoint(x=9, y=8, z=7)), + private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well(labware_id="123", well_name="A3"), + new_deck_point=DeckPoint(x=9, y=8, z=7), + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 1e24a8033f1..9b92fd219a0 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -9,7 +9,8 @@ from opentrons.protocol_engine.errors import TipNotAttachedError from opentrons.protocol_engine.execution import MovementHandler, TipHandler from opentrons.protocol_engine.resources import ModelUtils -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import TipGeometry from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData @@ -18,7 +19,6 @@ PickUpTipResult, PickUpTipImplementation, TipPhysicallyMissingError, - TipPhysicallyMissingErrorInternalData, ) @@ -73,6 +73,13 @@ async def test_success( position=DeckPoint(x=111, y=222, z=333), ), private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well(labware_id="labware-id", well_name="A3"), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ) + ), ) @@ -97,6 +104,14 @@ async def test_tip_physically_missing_error( error_id = "error-id" error_created_at = datetime(1234, 5, 6) + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + well_location=WellLocation(offset=WellOffset()), + ) + ).then_return(Point(x=111, y=222, z=333)) decoy.when( await tip_handler.pick_up_tip( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name @@ -113,7 +128,13 @@ async def test_tip_physically_missing_error( public=TipPhysicallyMissingError.construct( id=error_id, createdAt=error_created_at, wrappedErrors=[matchers.Anything()] ), - private=TipPhysicallyMissingErrorInternalData( - pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="labware-id", well_name="well-name" + ), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ) ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py b/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py index 8bafa40d47e..51b3f24b753 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py @@ -16,7 +16,7 @@ ) from opentrons.protocol_engine.execution import ReloadedLabwareData, EquipmentHandler from opentrons.protocol_engine.resources import labware_validation -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.reload_labware import ( @@ -30,7 +30,7 @@ def patch_mock_labware_validation( decoy: Decoy, monkeypatch: pytest.MonkeyPatch ) -> None: - """Mock out move_types.py functions.""" + """Mock out labware_validation.py functions.""" for name, func in inspect.getmembers(labware_validation, inspect.isfunction): monkeypatch.setattr(labware_validation, name, decoy.mock(func=func)) diff --git a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py index 2f440c96f13..f18e79e0a55 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py @@ -6,7 +6,8 @@ from opentrons.motion_planning import Waypoint from opentrons.protocol_engine import WellLocation, WellOffset, DeckPoint, errors from opentrons.protocol_engine.execution import MovementHandler, GantryMover -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point from opentrons.protocol_engine.commands.command import SuccessData @@ -122,7 +123,15 @@ async def test_touch_tip_implementation( result = await subject.execute(params) assert result == SuccessData( - public=TouchTipResult(position=DeckPoint(x=4, y=5, z=6)), private=None + public=TouchTipResult(position=DeckPoint(x=4, y=5, z=6)), + private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well(labware_id="123", well_name="A3"), + new_deck_point=DeckPoint(x=4, y=5, z=6), + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_close_lid.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_close_lid.py index a569c18c970..101af301946 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_close_lid.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_close_lid.py @@ -4,7 +4,7 @@ from opentrons.hardware_control.modules import Thermocycler from opentrons.protocol_engine.types import MotorAxis -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_block.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_block.py index 75627b93014..9f4ced905dc 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_block.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_block.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import Thermocycler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_lid.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_lid.py index 11d6e292370..1ea0e218c06 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_lid.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_lid.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import Thermocycler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_open_lid.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_open_lid.py index 8be2cd89c2d..2d665138306 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_open_lid.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_open_lid.py @@ -4,7 +4,7 @@ from opentrons.hardware_control.modules import Thermocycler from opentrons.protocol_engine.types import MotorAxis -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py index d97bacf7c85..9d3b79c66b1 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import Thermocycler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py index 89e00592510..f05ac55c0ee 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import Thermocycler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_lid_temperature.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_lid_temperature.py index aa558561ac8..b3565bc8a2d 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_lid_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_lid_temperature.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import Thermocycler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_block_temperature.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_block_temperature.py index 060cc34f2c2..880729bc149 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_block_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_block_temperature.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import Thermocycler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_lid_temperature.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_lid_temperature.py index 08ad7db94a9..47b4b006342 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_lid_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_lid_temperature.py @@ -3,7 +3,7 @@ from opentrons.hardware_control.modules import Thermocycler -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleSubState, ThermocyclerModuleId, diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py new file mode 100644 index 00000000000..0130d7ce16b --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py @@ -0,0 +1,52 @@ +"""Test update-position-estimator commands.""" +from decoy import Decoy + +from opentrons.protocol_engine.commands.unsafe.unsafe_engage_axes import ( + UnsafeEngageAxesParams, + UnsafeEngageAxesResult, + UnsafeEngageAxesImplementation, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.execution import GantryMover +from opentrons.protocol_engine.types import MotorAxis +from opentrons.hardware_control import OT3HardwareControlAPI +from opentrons.hardware_control.types import Axis + + +async def test_engage_axes_implementation( + decoy: Decoy, ot3_hardware_api: OT3HardwareControlAPI, gantry_mover: GantryMover +) -> None: + """Test EngageAxes command execution.""" + subject = UnsafeEngageAxesImplementation( + hardware_api=ot3_hardware_api, gantry_mover=gantry_mover + ) + + data = UnsafeEngageAxesParams( + axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y] + ) + + decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return( + Axis.Z_L + ) + decoy.when( + gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER) + ).then_return(Axis.P_L) + decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return( + Axis.X + ) + decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( + Axis.Y + ) + decoy.when( + await ot3_hardware_api.update_axis_position_estimations( + [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y] + ) + ).then_return(None) + + result = await subject.execute(data) + + assert result == SuccessData(public=UnsafeEngageAxesResult(), private=None) + + decoy.verify( + await ot3_hardware_api.engage_axes([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py index f25d8d06169..a40f914e049 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py @@ -2,7 +2,7 @@ from decoy import Decoy from opentrons.types import MountType -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.commands.unsafe.unsafe_blow_out_in_place import ( UnsafeBlowOutInPlaceParams, UnsafeBlowOutInPlaceResult, diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py index 3659dd2db31..6d739f97442 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py @@ -3,7 +3,7 @@ from decoy import Decoy from opentrons.types import MountType -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import TipHandler diff --git a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py index bc2e8a0a8fe..2df3a2cdd25 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -19,7 +19,7 @@ EStopActivatedError as PE_EStopActivatedError, ) from opentrons.protocol_engine.resources import ModelUtils -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.actions import ( ActionDispatcher, RunCommandAction, @@ -219,7 +219,7 @@ class _TestCommandDefinedError(ErrorOccurrence): _TestCommandReturn = Union[ SuccessData[_TestCommandResult, None], - DefinedErrorData[_TestCommandDefinedError, None], + DefinedErrorData[_TestCommandDefinedError], ] @@ -561,7 +561,6 @@ class _TestCommand( error_id = "error-id" returned_error = DefinedErrorData( public=_TestCommandDefinedError(id=error_id, createdAt=failed_at), - private=None, ) queued_command = cast( Command, diff --git a/api/tests/opentrons/protocol_engine/execution/test_door_watcher.py b/api/tests/opentrons/protocol_engine/execution/test_door_watcher.py index 1e252650957..dcf3db10653 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_door_watcher.py +++ b/api/tests/opentrons/protocol_engine/execution/test_door_watcher.py @@ -16,7 +16,7 @@ ) from opentrons.protocol_engine.actions import ActionDispatcher, DoorChangeAction -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.execution.door_watcher import ( DoorWatcher, ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index d28ebe700ca..370b76a16dd 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -39,7 +39,8 @@ FlowRates, ) -from opentrons.protocol_engine.state import Config, StateStore +from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.state.modules import HardwareModule from opentrons.protocol_engine.resources import ( ModelUtils, diff --git a/api/tests/opentrons/protocol_engine/execution/test_gantry_mover.py b/api/tests/opentrons/protocol_engine/execution/test_gantry_mover.py index 01a3ca6e3a5..6f6d3274532 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_gantry_mover.py +++ b/api/tests/opentrons/protocol_engine/execution/test_gantry_mover.py @@ -15,7 +15,8 @@ from opentrons.motion_planning import Waypoint -from opentrons.protocol_engine.state import StateView, PipetteLocationData +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.motion import PipetteLocationData from opentrons.protocol_engine.types import MotorAxis, DeckPoint, CurrentWell from opentrons.protocol_engine.errors import MustHomeError, InvalidAxisForRobotType diff --git a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py index 537fd07613c..4c3e629d2ed 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py +++ b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py @@ -9,7 +9,7 @@ from opentrons.hardware_control.types import OT3Mount from opentrons.types import PipetteNotAttachedError as HwPipetteNotAttachedError -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.execution import ( MovementHandler, TipHandler, diff --git a/api/tests/opentrons/protocol_engine/execution/test_heater_shaker_movement_flagger.py b/api/tests/opentrons/protocol_engine/execution/test_heater_shaker_movement_flagger.py index 91afa9f023c..e5d07fec8eb 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_heater_shaker_movement_flagger.py +++ b/api/tests/opentrons/protocol_engine/execution/test_heater_shaker_movement_flagger.py @@ -26,7 +26,7 @@ from opentrons.protocol_engine.execution.heater_shaker_movement_flagger import ( HeaterShakerMovementFlagger, ) -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.state.module_substates.heater_shaker_module_substate import ( HeaterShakerModuleId, HeaterShakerModuleSubState, diff --git a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py index 58619647f54..c434995ee52 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py @@ -44,7 +44,7 @@ ThermocyclerNotOpenError, HeaterShakerLabwareLatchNotOpenError, ) -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore if TYPE_CHECKING: from opentrons.hardware_control.ot3api import OT3API @@ -87,9 +87,9 @@ def heater_shaker_movement_flagger(decoy: Decoy) -> HeaterShakerMovementFlagger: @pytest.fixture -def hardware_gripper_offset_data() -> Tuple[ - LabwareMovementOffsetData, LabwareMovementOffsetData -]: +def hardware_gripper_offset_data() -> ( + Tuple[LabwareMovementOffsetData, LabwareMovementOffsetData] +): """Get a set of mocked labware offset data.""" user_offset_data = LabwareMovementOffsetData( pickUpOffset=LabwareOffsetVector(x=123, y=234, z=345), diff --git a/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py index 75205b6e45d..7737775c4fb 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py @@ -19,10 +19,10 @@ MotorAxis, AddressableOffsetVector, ) -from opentrons.protocol_engine.state import ( +from opentrons.protocol_engine.state.state import ( StateStore, - PipetteLocationData, ) +from opentrons.protocol_engine.state.motion import PipetteLocationData from opentrons.protocol_engine.execution.movement import MovementHandler from opentrons.protocol_engine.execution.thermocycler_movement_flagger import ( ThermocyclerMovementFlagger, diff --git a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py index b087084abff..84a425b88fc 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py @@ -8,7 +8,8 @@ from opentrons.hardware_control import API as HardwareAPI from opentrons.hardware_control.dev_types import PipetteDict -from opentrons.protocol_engine.state import StateView, HardwarePipette +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.pipettes import HardwarePipette from opentrons.protocol_engine.types import TipGeometry from opentrons.protocol_engine.execution.pipetting import ( HardwarePipettingHandler, diff --git a/api/tests/opentrons/protocol_engine/execution/test_queue_worker.py b/api/tests/opentrons/protocol_engine/execution/test_queue_worker.py index aba78a1fb37..e625a5c26b8 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_queue_worker.py +++ b/api/tests/opentrons/protocol_engine/execution/test_queue_worker.py @@ -4,7 +4,7 @@ import pytest from decoy import Decoy, matchers -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.errors import RunStoppedError from opentrons.protocol_engine.execution import CommandExecutor, QueueWorker diff --git a/api/tests/opentrons/protocol_engine/execution/test_run_control_handler.py b/api/tests/opentrons/protocol_engine/execution/test_run_control_handler.py index b8a51537314..f21c133d485 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_run_control_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_run_control_handler.py @@ -6,7 +6,8 @@ from opentrons.protocol_engine.actions import ActionDispatcher, PauseAction, PauseSource from opentrons.protocol_engine.execution.run_control import RunControlHandler -from opentrons.protocol_engine.state import Config, StateStore +from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.types import DeckType diff --git a/api/tests/opentrons/protocol_engine/execution/test_thermocycler_movement_flagger.py b/api/tests/opentrons/protocol_engine/execution/test_thermocycler_movement_flagger.py index ac8bf1743f4..415ff038b09 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_thermocycler_movement_flagger.py +++ b/api/tests/opentrons/protocol_engine/execution/test_thermocycler_movement_flagger.py @@ -8,7 +8,7 @@ from decoy import Decoy from opentrons.types import DeckSlotName -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.state.module_substates.thermocycler_module_substate import ( ThermocyclerModuleId, ThermocyclerModuleSubState, diff --git a/api/tests/opentrons/protocol_engine/execution/test_thermocycler_plate_lifter.py b/api/tests/opentrons/protocol_engine/execution/test_thermocycler_plate_lifter.py index 67f8cee04b8..7f05fd070a8 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_thermocycler_plate_lifter.py +++ b/api/tests/opentrons/protocol_engine/execution/test_thermocycler_plate_lifter.py @@ -13,8 +13,8 @@ from opentrons.protocol_engine.execution.thermocycler_plate_lifter import ( ThermocyclerPlateLifter, ) -from opentrons.protocol_engine.state import ( - StateStore, +from opentrons.protocol_engine.state.state import StateStore +from opentrons.protocol_engine.state.module_substates import ( ThermocyclerModuleId, ThermocyclerModuleSubState, ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index dfd02e9dfd5..560675b8190 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -12,7 +12,7 @@ from opentrons.hardware_control.protocols.types import OT2RobotType, FlexRobotType from opentrons.protocols.models import LabwareDefinition -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import TipGeometry, TipPresenceStatus from opentrons.protocol_engine.resources import LabwareDataProvider from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError @@ -121,7 +121,6 @@ async def test_flex_pick_up_tip_state( with patch.object( ot3_hardware_api, "cache_tip", AsyncMock(spec=ot3_hardware_api.cache_tip) ) as mock_add_tip: - if tip_state == TipStateType.PRESENT: await subject.pick_up_tip( pipette_id="pipette-id", diff --git a/api/tests/opentrons/protocol_engine/mock_circular_frusta.py b/api/tests/opentrons/protocol_engine/mock_circular_frusta.py new file mode 100644 index 00000000000..7586cb604b8 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/mock_circular_frusta.py @@ -0,0 +1,215 @@ +"""Mock representations of potential circular frusta.""" +""" +These are circular frusta whose radii either decay or grow, but always at a constant rate. +Height always decays from the max height to 0 in increments of 1. +""" +example_1 = { + "height": [ + 26, + 25, + 24, + 23, + 22, + 21, + 20, + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + ], + "radius": [ + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + ], +} +example_2 = { + "height": [18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], + "radius": [ + 67, + 68.5, + 70, + 71.5, + 73, + 74.5, + 76, + 77.5, + 79, + 80.5, + 82, + 83.5, + 85, + 86.5, + 88, + 89.5, + 91, + 92.5, + 94, + ], +} +example_3 = { + "height": [ + 20, + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + ], + "radius": [ + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + ], +} +example_4 = { + "height": [ + 34, + 33, + 32, + 31, + 30, + 29, + 28, + 27, + 26, + 25, + 24, + 23, + 22, + 21, + 20, + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + ], + "radius": [ + 280, + 274.5, + 269, + 263.5, + 258, + 252.5, + 247, + 241.5, + 236, + 230.5, + 225, + 219.5, + 214, + 208.5, + 203, + 197.5, + 192, + 186.5, + 181, + 175.5, + 170, + 164.5, + 159, + 153.5, + 148, + 142.5, + 137, + 131.5, + 126, + 120.5, + 115, + 109.5, + 104, + 98.5, + 93, + ], +} + +TEST_EXAMPLES = [example_1, example_2, example_3, example_4] diff --git a/api/tests/opentrons/protocol_engine/mock_rectangular_frusta.py b/api/tests/opentrons/protocol_engine/mock_rectangular_frusta.py new file mode 100644 index 00000000000..56951ff3219 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/mock_rectangular_frusta.py @@ -0,0 +1,326 @@ +"""Mock representations of potential rectangular frusta.""" +""" +These are rectangular frusta whose length and width decay regularly, though not necessarily at the same rate. +Height always decays from the max height to 0 in increments of 1. +This has frusta with widths and lengths that both grow and decay with respect to positive change in height. +""" +example_1 = { + "height": [ + 26, + 25, + 24, + 23, + 22, + 21, + 20, + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + ], + "length": [ + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + ], + "width": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + ], +} +example_2 = { + "height": [18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], + "length": [ + 67, + 68.5, + 70, + 71.5, + 73, + 74.5, + 76, + 77.5, + 79, + 80.5, + 82, + 83.5, + 85, + 86.5, + 88, + 89.5, + 91, + 92.5, + 94, + ], + "width": [ + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50, + ], +} +example_3 = { + "height": [ + 20, + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + ], + "length": [ + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + ], + "width": [ + 600, + 575, + 550, + 525, + 500, + 475, + 450, + 425, + 400, + 375, + 350, + 325, + 300, + 275, + 250, + 225, + 200, + 175, + 150, + 125, + 100, + ], +} +example_4 = { + "height": [ + 34, + 33, + 32, + 31, + 30, + 29, + 28, + 27, + 26, + 25, + 24, + 23, + 22, + 21, + 20, + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + ], + "length": [ + 280, + 274.5, + 269, + 263.5, + 258, + 252.5, + 247, + 241.5, + 236, + 230.5, + 225, + 219.5, + 214, + 208.5, + 203, + 197.5, + 192, + 186.5, + 181, + 175.5, + 170, + 164.5, + 159, + 153.5, + 148, + 142.5, + 137, + 131.5, + 126, + 120.5, + 115, + 109.5, + 104, + 98.5, + 93, + ], + "width": [ + 280, + 274.5, + 269, + 263.5, + 258, + 252.5, + 247, + 241.5, + 236, + 230.5, + 225, + 219.5, + 214, + 208.5, + 203, + 197.5, + 192, + 186.5, + 181, + 175.5, + 170, + 164.5, + 159, + 153.5, + 148, + 142.5, + 137, + 131.5, + 126, + 120.5, + 115, + 109.5, + 104, + 98.5, + 93, + ], +} + +TEST_EXAMPLES = [example_1, example_2, example_3, example_4] diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index 4fb2f6a2fd3..086b3ec297b 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -93,7 +93,7 @@ def test_configure_virtual_pipette_for_volume( nozzle_map=result1.nozzle_map, back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), - pipette_lld_settings={"t50": {"minHeight": 0.5, "minVolume": 0.0}}, + pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, ) subject_instance.configure_virtual_pipette_for_volume( "my-pipette", 1, result1.model @@ -119,7 +119,7 @@ def test_configure_virtual_pipette_for_volume( nozzle_map=result2.nozzle_map, back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), - pipette_lld_settings={"t50": {"minHeight": 0.5, "minVolume": 0.0}}, + pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, ) diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index 845b33f18d8..df7fb4dca9a 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -295,6 +295,32 @@ def create_dispense_in_place_command( ) +def create_liquid_probe_command( + pipette_id: str = "pippete-id", + labware_id: str = "labware-id", + well_name: str = "well-name", + well_location: Optional[WellLocation] = None, + destination: DeckPoint = DeckPoint(x=0, y=0, z=0), +) -> cmd.LiquidProbe: + """Get a completed Liquid Probe command.""" + params = cmd.LiquidProbeParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location or WellLocation(), + ) + result = cmd.LiquidProbeResult(position=destination, z_position=0.5) + + return cmd.LiquidProbe( + id="command-id", + key="command-key", + status=cmd.CommandStatus.SUCCEEDED, + createdAt=datetime.now(), + params=params, + result=result, + ) + + def create_pick_up_tip_command( pipette_id: str, labware_id: str = "labware-id", diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py index 987db0dcba3..da3e0f3d156 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py @@ -33,6 +33,12 @@ def test_deck_configuration_setting( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index 9c098cf1c96..b259e6a3f96 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -17,7 +17,7 @@ SucceedCommandAction, AddAddressableAreaAction, ) -from opentrons.protocol_engine.state import Config +from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.state.addressable_areas import ( AddressableAreaStore, AddressableAreaState, @@ -74,6 +74,12 @@ def simulated_subject( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], @@ -101,6 +107,12 @@ def subject( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], @@ -127,6 +139,12 @@ def test_initial_state_simulated( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py index 07552aa4273..30ca1b9e7c4 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -69,6 +69,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_command_history.py b/api/tests/opentrons/protocol_engine/state/test_command_history.py index 753d69654e1..14eaa2a42f3 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_history.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_history.py @@ -13,10 +13,14 @@ def create_queued_command_entry( - command_id: str = "command-id", index: int = 0 + command_id: str = "command-id", + intent: CommandIntent = CommandIntent.PROTOCOL, + index: int = 0, ) -> CommandEntry: """Create a command entry for a queued command.""" - return CommandEntry(create_queued_command(command_id=command_id), index) + return CommandEntry( + create_queued_command(command_id=command_id, intent=intent), index + ) def create_fixit_command_entry( @@ -262,3 +266,54 @@ def test_remove_id_from_setup_queue(command_history: CommandHistory) -> None: command_history._add_to_setup_queue("1") command_history._remove_setup_queue_id("0") assert command_history.get_setup_queue_ids() == OrderedSet(["1"]) + + +def test_get_filtered_commands(command_history: CommandHistory) -> None: + """It should return a list of all commands without fixit commands.""" + assert ( + list(command_history.get_filtered_command_ids(include_fixit_commands=False)) + == [] + ) + command_entry_1 = create_queued_command(command_id="0") + command_entry_2 = create_queued_command(command_id="1") + fixit_command_entry_1 = create_queued_command( + intent=CommandIntent.FIXIT, command_id="fixit-1" + ) + command_history.append_queued_command(command_entry_1) + command_history.append_queued_command(command_entry_2) + command_history.append_queued_command(fixit_command_entry_1) + assert list( + command_history.get_filtered_command_ids(include_fixit_commands=False) + ) == ["0", "1"] + + +def test_get_all_filtered_commands(command_history: CommandHistory) -> None: + """It should return a list of all commands without fixit commands.""" + assert ( + list(command_history.get_filtered_command_ids(include_fixit_commands=False)) + == [] + ) + command_entry_1 = create_queued_command_entry() + command_entry_2 = create_queued_command_entry(index=1, intent=CommandIntent.SETUP) + fixit_command_entry_1 = create_queued_command_entry(intent=CommandIntent.FIXIT) + command_history._add("0", command_entry_1) + command_history._add("1", command_entry_2) + command_history._add("fixit-1", fixit_command_entry_1) + assert list( + command_history.get_filtered_command_ids(include_fixit_commands=True) + ) == ["0", "1", "fixit-1"] + + +def test_get_slice_with_filtered_list(command_history: CommandHistory) -> None: + """It should return a slice of filtered commands.""" + assert command_history.get_slice(0, 2) == [] + command_entry_1 = create_queued_command_entry() + command_entry_2 = create_queued_command_entry(index=1) + command_entry_3 = create_queued_command_entry(index=2) + command_history._add("0", command_entry_1) + command_history._add("1", command_entry_2) + command_history._add("2", command_entry_3) + filtered_list = ["0", "1"] + assert command_history.get_slice(1, 3, command_ids=filtered_list) == [ + command_entry_2.command, + ] diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py index 018634db435..4b7cf01e87c 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -17,7 +17,7 @@ from opentrons.protocol_engine import commands, errors from opentrons.protocol_engine.types import DeckType -from opentrons.protocol_engine.state import Config +from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.state.commands import ( CommandState, CommandStore, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py index 5aa7d04a2ee..48918eec6eb 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -81,6 +81,7 @@ def get_command_view( # noqa: C901 if running_command_id: command_history._set_running_command_id(running_command_id) + # TODO(tz, 8-21-24): consolidate all quques into 1 and use append_queued_command if queued_command_ids: for command_id in queued_command_ids: command_history._add_to_queue(command_id) @@ -904,7 +905,7 @@ def test_get_current() -> None: def test_get_slice_empty() -> None: """It should return a slice from the tail if no current command.""" subject = get_command_view(commands=[]) - result = subject.get_slice(cursor=0, length=2) + result = subject.get_slice(cursor=0, length=2, include_fixit_commands=True) assert result == CommandSlice(commands=[], cursor=0, total_length=0) @@ -918,7 +919,7 @@ def test_get_slice() -> None: subject = get_command_view(commands=[command_1, command_2, command_3, command_4]) - result = subject.get_slice(cursor=1, length=3) + result = subject.get_slice(cursor=1, length=3, include_fixit_commands=True) assert result == CommandSlice( commands=[command_2, command_3, command_4], @@ -926,7 +927,7 @@ def test_get_slice() -> None: total_length=4, ) - result = subject.get_slice(cursor=-3, length=10) + result = subject.get_slice(cursor=-3, length=10, include_fixit_commands=True) assert result == CommandSlice( commands=[command_1, command_2, command_3, command_4], @@ -944,7 +945,7 @@ def test_get_slice_default_cursor_no_current() -> None: subject = get_command_view(commands=[command_1, command_2, command_3, command_4]) - result = subject.get_slice(cursor=None, length=3) + result = subject.get_slice(cursor=None, length=3, include_fixit_commands=True) assert result == CommandSlice( commands=[command_2, command_3, command_4], @@ -975,7 +976,7 @@ def test_get_slice_default_cursor_failed_command() -> None: failed_command=CommandEntry(index=2, command=command_3), ) - result = subject.get_slice(cursor=None, length=3) + result = subject.get_slice(cursor=None, length=3, include_fixit_commands=True) assert result == CommandSlice( commands=[command_3, command_4], @@ -997,7 +998,7 @@ def test_get_slice_default_cursor_running() -> None: running_command_id="command-id-3", ) - result = subject.get_slice(cursor=None, length=2) + result = subject.get_slice(cursor=None, length=2, include_fixit_commands=True) assert result == CommandSlice( commands=[command_3, command_4], @@ -1040,3 +1041,48 @@ def test_get_errors_slice() -> None: cursor=0, total_length=4, ) + + +def test_get_slice_without_fixit() -> None: + """It should select a cursor based on the running command, if present.""" + command_1 = create_succeeded_command(command_id="command-id-1") + command_2 = create_succeeded_command(command_id="command-id-2") + command_3 = create_running_command(command_id="command-id-3") + command_4 = create_queued_command(command_id="command-id-4") + command_5 = create_queued_command(command_id="command-id-5") + command_6 = create_queued_command( + command_id="fixit-id-1", intent=cmd.CommandIntent.FIXIT + ) + command_7 = create_queued_command( + command_id="fixit-id-2", intent=cmd.CommandIntent.FIXIT + ) + + subject = get_command_view( + commands=[ + command_1, + command_2, + command_3, + command_4, + command_5, + command_6, + command_7, + ], + queued_command_ids=[ + "command-id-1", + "command-id-2", + "command-id-3", + "command-id-4", + "command-id-5", + "fixit-id-1", + "fixit-id-2", + ], + queued_fixit_command_ids=["fixit-id-1", "fixit-id-2"], + ) + + result = subject.get_slice(cursor=None, length=7, include_fixit_commands=False) + + assert result == CommandSlice( + commands=[command_1, command_2, command_3, command_4, command_5], + cursor=0, + total_length=5, + ) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index f23d8f4a6e1..1854d08523a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1,10 +1,10 @@ """Test state getters for retrieving geometry views of state.""" import inspect - import json import pytest +from math import isclose from decoy import Decoy -from typing import cast, List, Tuple, Optional, NamedTuple +from typing import cast, List, Tuple, Optional, NamedTuple, Dict from datetime import datetime from opentrons_shared_data.deck.types import DeckDefinitionV5 @@ -61,9 +61,10 @@ LoadModuleParams, ) from opentrons.protocol_engine.actions import SucceedCommandAction -from opentrons.protocol_engine.state import move_types +from opentrons.protocol_engine.state import _move_types from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.state.labware import LabwareView, LabwareStore +from opentrons.protocol_engine.state.wells import WellView, WellStore from opentrons.protocol_engine.state.modules import ModuleView, ModuleStore from opentrons.protocol_engine.state.pipettes import ( PipetteView, @@ -77,7 +78,15 @@ AddressableAreaStore, ) from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType +from opentrons.protocol_engine.state.frustum_helpers import ( + height_from_volume_circular, + height_from_volume_rectangular, + volume_from_height_circular, + volume_from_height_rectangular, +) from ..pipette_fixtures import get_default_nozzle_map +from ..mock_circular_frusta import TEST_EXAMPLES as CIRCULAR_TEST_EXAMPLES +from ..mock_rectangular_frusta import TEST_EXAMPLES as RECTANGULAR_TEST_EXAMPLES @pytest.fixture @@ -86,6 +95,12 @@ def mock_labware_view(decoy: Decoy) -> LabwareView: return decoy.mock(cls=LabwareView) +@pytest.fixture +def mock_well_view(decoy: Decoy) -> WellView: + """Get a mock in the shape of a WellView.""" + return decoy.mock(cls=WellView) + + @pytest.fixture def mock_module_view(decoy: Decoy) -> ModuleView: """Get a mock in the shape of a ModuleView.""" @@ -105,10 +120,10 @@ def mock_addressable_area_view(decoy: Decoy) -> AddressableAreaView: @pytest.fixture(autouse=True) -def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: - """Mock out move_types.py functions.""" - for name, func in inspect.getmembers(move_types, inspect.isfunction): - monkeypatch.setattr(move_types, name, decoy.mock(func=func)) +def patch_mock__move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: + """Mock out _move_types.py functions.""" + for name, func in inspect.getmembers(_move_types, inspect.isfunction): + monkeypatch.setattr(_move_types, name, decoy.mock(func=func)) @pytest.fixture @@ -144,6 +159,18 @@ def labware_view(labware_store: LabwareStore) -> LabwareView: return LabwareView(labware_store._state) +@pytest.fixture +def well_store() -> WellStore: + """Get a well store that can accept actions.""" + return WellStore() + + +@pytest.fixture +def well_view(well_store: WellStore) -> WellView: + """Get a well view of a real well store.""" + return WellView(well_store._state) + + @pytest.fixture def module_store(state_config: Config) -> ModuleStore: """Get a module store that can accept actions.""" @@ -184,6 +211,12 @@ def addressable_area_store( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], @@ -228,11 +261,13 @@ def nice_adapter_definition() -> LabwareDefinition: @pytest.fixture def subject( mock_labware_view: LabwareView, + mock_well_view: WellView, mock_module_view: ModuleView, mock_pipette_view: PipetteView, mock_addressable_area_view: AddressableAreaView, state_config: Config, labware_view: LabwareView, + well_view: WellView, module_view: ModuleView, pipette_view: PipetteView, addressable_area_view: AddressableAreaView, @@ -253,6 +288,7 @@ def my_cool_test(subject: GeometryView) -> None: return GeometryView( config=state_config, labware_view=mock_labware_view if use_mocks else labware_view, + well_view=mock_well_view if use_mocks else well_view, module_view=mock_module_view if use_mocks else module_view, pipette_view=mock_pipette_view if use_mocks else pipette_view, addressable_area_view=mock_addressable_area_view @@ -1463,6 +1499,59 @@ def test_get_well_position_with_center_offset( ) +def test_get_well_position_with_meniscus_offset( + decoy: Decoy, + well_plate_def: LabwareDefinition, + mock_labware_view: LabwareView, + mock_well_view: WellView, + mock_addressable_area_view: AddressableAreaView, + subject: GeometryView, +) -> None: + """It should be able to get the position of a well center in a labware.""" + labware_data = LoadedLabware( + id="labware-id", + loadName="load-name", + definitionUri="definition-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + offsetId="offset-id", + ) + calibration_offset = LabwareOffsetVector(x=1, y=-2, z=3) + slot_pos = Point(4, 5, 6) + well_def = well_plate_def.wells["B2"] + + decoy.when(mock_labware_view.get("labware-id")).then_return(labware_data) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + decoy.when(mock_labware_view.get_labware_offset_vector("labware-id")).then_return( + calibration_offset + ) + decoy.when( + mock_addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) + decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( + well_def + ) + decoy.when( + mock_well_view.get_last_measured_liquid_height("labware-id", "B2") + ).then_return(70.5) + + result = subject.get_well_position( + labware_id="labware-id", + well_name="B2", + well_location=WellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=2, y=3, z=4), + ), + ) + + assert result == Point( + x=slot_pos[0] + 1 + well_def.x + 2, + y=slot_pos[1] - 2 + well_def.y + 3, + z=slot_pos[2] + 3 + well_def.z + 4 + 70.5, + ) + + def test_get_relative_well_location( decoy: Decoy, well_plate_def: LabwareDefinition, @@ -2618,3 +2707,79 @@ def test_get_offset_fails_with_off_deck_labware( labware_store.handle_action(action) offset_location = subject.get_offset_location("labware-id-1") assert offset_location is None + + +@pytest.mark.parametrize("frustum", RECTANGULAR_TEST_EXAMPLES) +def test_rectangular_frustum_math_helpers( + decoy: Decoy, + frustum: Dict[str, List[float]], + subject: GeometryView, +) -> None: + """Test both height and volume calculation within a given rectangular frustum.""" + total_frustum_height = frustum["height"][0] + bottom_length = frustum["length"][-1] + bottom_width = frustum["width"][-1] + + def _find_volume_from_height_(index: int) -> None: + nonlocal total_frustum_height, bottom_width, bottom_length + top_length = frustum["length"][index] + top_width = frustum["width"][index] + target_height = frustum["height"][index] + + found_volume = volume_from_height_rectangular( + target_height=target_height, + total_frustum_height=total_frustum_height, + top_length=top_length, + bottom_length=bottom_length, + top_width=top_width, + bottom_width=bottom_width, + ) + + found_height = height_from_volume_rectangular( + volume=found_volume, + total_frustum_height=total_frustum_height, + top_length=top_length, + bottom_length=bottom_length, + top_width=top_width, + bottom_width=bottom_width, + ) + + assert isclose(found_height, frustum["height"][index]) + + for i in range(len(frustum["height"])): + _find_volume_from_height_(i) + + +@pytest.mark.parametrize("frustum", CIRCULAR_TEST_EXAMPLES) +def test_circular_frustum_math_helpers( + decoy: Decoy, + frustum: Dict[str, List[float]], + subject: GeometryView, +) -> None: + """Test both height and volume calculation within a given circular frustum.""" + total_frustum_height = frustum["height"][0] + bottom_radius = frustum["radius"][-1] + + def _find_volume_from_height_(index: int) -> None: + nonlocal total_frustum_height, bottom_radius + top_radius = frustum["radius"][index] + target_height = frustum["height"][index] + + found_volume = volume_from_height_circular( + target_height=target_height, + total_frustum_height=total_frustum_height, + top_radius=top_radius, + bottom_radius=bottom_radius, + ) + + found_height = height_from_volume_circular( + volume=found_volume, + total_frustum_height=total_frustum_height, + top_radius=top_radius, + bottom_radius=bottom_radius, + ) + + assert isclose(found_height, frustum["height"][index]) + + for i in range(len(frustum["height"])): + _find_volume_from_height_(i) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index 43c69594422..ab2f49cfb29 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -40,7 +40,7 @@ OverlapOffset, LabwareMovementOffsetData, ) -from opentrons.protocol_engine.state.move_types import EdgePathType +from opentrons.protocol_engine.state._move_types import EdgePathType from opentrons.protocol_engine.state.labware import ( LabwareState, LabwareView, @@ -1264,7 +1264,9 @@ def test_raise_if_labware_inaccessible_by_pipette_off_deck() -> None: subject.raise_if_labware_inaccessible_by_pipette("labware-id") -def test_raise_if_labware_inaccessible_by_pipette_stacked_labware_on_staging_area() -> None: +def test_raise_if_labware_inaccessible_by_pipette_stacked_labware_on_staging_area() -> ( + None +): """It should raise if the labware is stacked on a staging slot.""" subject = get_labware_view( labware_by_id={ diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store.py b/api/tests/opentrons/protocol_engine/state/test_module_store.py index 5a26fc97d1a..4f94ed314d5 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_store.py @@ -79,6 +79,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index 95b868497d2..3a5f14f1516 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -92,6 +92,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_motion_view.py b/api/tests/opentrons/protocol_engine/state/test_motion_view.py index 278fff82023..703d813373b 100644 --- a/api/tests/opentrons/protocol_engine/state/test_motion_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_motion_view.py @@ -20,13 +20,13 @@ MotorAxis, AddressableOffsetVector, ) -from opentrons.protocol_engine.state import PipetteLocationData, move_types +from opentrons.protocol_engine.state import _move_types from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.state.labware import LabwareView from opentrons.protocol_engine.state.pipettes import PipetteView from opentrons.protocol_engine.state.addressable_areas import AddressableAreaView from opentrons.protocol_engine.state.geometry import GeometryView -from opentrons.protocol_engine.state.motion import MotionView +from opentrons.protocol_engine.state.motion import MotionView, PipetteLocationData from opentrons.protocol_engine.state.modules import ModuleView from opentrons.protocol_engine.state.module_substates import HeaterShakerModuleId from opentrons_shared_data.robot.types import RobotType @@ -46,10 +46,10 @@ def patch_mock_get_waypoints(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> N @pytest.fixture(autouse=True) -def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: - """Mock out move_types.py functions.""" - for name, func in inspect.getmembers(move_types, inspect.isfunction): - monkeypatch.setattr(move_types, name, decoy.mock(func=func)) +def patch_mock__move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: + """Mock out _move_types.py functions.""" + for name, func in inspect.getmembers(_move_types, inspect.isfunction): + monkeypatch.setattr(_move_types, name, decoy.mock(func=func)) @pytest.fixture @@ -313,7 +313,7 @@ def test_get_movement_waypoints_to_well_for_y_center( ).then_return(Point(x=4, y=5, z=6)) decoy.when( - move_types.get_move_type_to_well( + _move_types.get_move_type_to_well( "pipette-id", "labware-id", "well-name", location, True ) ).then_return(motion_planning.MoveType.GENERAL_ARC) @@ -395,7 +395,7 @@ def test_get_movement_waypoints_to_well_for_xy_center( ).then_return(Point(x=4, y=5, z=6)) decoy.when( - move_types.get_move_type_to_well( + _move_types.get_move_type_to_well( "pipette-id", "labware-id", "well-name", location, True ) ).then_return(motion_planning.MoveType.GENERAL_ARC) @@ -910,18 +910,18 @@ def test_get_touch_tip_waypoints( labware_view.get_edge_path_type( "labware-id", "B2", MountType.LEFT, DeckSlotName.SLOT_4, True ) - ).then_return(move_types.EdgePathType.RIGHT) + ).then_return(_move_types.EdgePathType.RIGHT) decoy.when( labware_view.get_well_radial_offsets("labware-id", "B2", 0.123) ).then_return((1.2, 3.4)) decoy.when( - move_types.get_edge_point_list( + _move_types.get_edge_point_list( center=center_point, x_radius=1.2, y_radius=3.4, - edge_path_type=move_types.EdgePathType.RIGHT, + edge_path_type=_move_types.EdgePathType.RIGHT, ) ).then_return([Point(x=11, y=22, z=33), Point(x=44, y=55, z=66)]) diff --git a/api/tests/opentrons/protocol_engine/state/test_move_types.py b/api/tests/opentrons/protocol_engine/state/test_move_types.py index 875ec0483ba..9d46cb8a1ab 100644 --- a/api/tests/opentrons/protocol_engine/state/test_move_types.py +++ b/api/tests/opentrons/protocol_engine/state/test_move_types.py @@ -4,7 +4,7 @@ from opentrons.types import Point from opentrons.motion_planning.types import MoveType -from opentrons.protocol_engine.state import move_types as subject +from opentrons.protocol_engine.state import _move_types as subject from opentrons.protocol_engine.types import CurrentWell diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py index a49c9255605..22c58b77e5b 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -6,17 +6,11 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.pipette import pipette_definition +from opentrons.protocol_engine.state import update_types from opentrons.types import DeckSlotName, MountType, Point from opentrons.protocol_engine import commands as cmd -from opentrons.protocol_engine.commands.command import DefinedErrorData -from opentrons.protocol_engine.commands.pipetting_common import ( - LiquidNotFoundError, - LiquidNotFoundErrorInternalData, - OverpressureError, - OverpressureErrorInternalData, -) -from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.types import ( + CurrentAddressableArea, DeckPoint, DeckSlotLocation, LoadedPipette, @@ -52,8 +46,8 @@ create_pick_up_tip_command, create_drop_tip_command, create_drop_tip_in_place_command, + create_succeeded_command, create_unsafe_drop_tip_in_place_command, - create_touch_tip_command, create_move_to_well_command, create_blow_out_command, create_blow_out_in_place_command, @@ -72,6 +66,35 @@ def subject() -> PipetteStore: return PipetteStore() +def _create_move_to_well_action( + pipette_id: str, + labware_id: str, + well_name: str, + deck_point: DeckPoint, +) -> SucceedCommandAction: + command = create_move_to_well_command( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + destination=deck_point, + ) + action = SucceedCommandAction( + command=command, + private_result=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=update_types.Well( + labware_id=labware_id, + well_name=well_name, + ), + new_deck_point=deck_point, + ) + ), + ) + return action + + def test_sets_initial_state(subject: PipetteStore) -> None: """It should initialize its state object properly.""" result = subject.state @@ -90,6 +113,85 @@ def test_sets_initial_state(subject: PipetteStore) -> None: ) +def test_location_state_update(subject: PipetteStore) -> None: + """It should update pipette locations.""" + load_command = create_load_pipette_command( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.RIGHT, + ) + subject.handle_action( + SucceedCommandAction(command=load_command, private_result=None) + ) + + # Update the location to a well: + dummy_command = create_succeeded_command() + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + private_result=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="come on barbie", + well_name="let's go party", + ), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ) + ), + ) + ) + assert subject.state.current_location == CurrentWell( + pipette_id="pipette-id", labware_id="come on barbie", well_name="let's go party" + ) + assert subject.state.current_deck_point == CurrentDeckPoint( + mount=MountType.RIGHT, deck_point=DeckPoint(x=111, y=222, z=333) + ) + + # Update the location to an addressable area: + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + private_result=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.AddressableArea( + addressable_area_name="na na na na na" + ), + new_deck_point=DeckPoint(x=333, y=444, z=555), + ) + ), + ) + ) + assert subject.state.current_location == CurrentAddressableArea( + pipette_id="pipette-id", addressable_area_name="na na na na na" + ) + assert subject.state.current_deck_point == CurrentDeckPoint( + mount=MountType.RIGHT, deck_point=DeckPoint(x=333, y=444, z=555) + ) + + # Clear the logical location: + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + private_result=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=None, + new_deck_point=update_types.NO_CHANGE, + ) + ), + ) + ) + assert subject.state.current_location is None + assert subject.state.current_deck_point == CurrentDeckPoint( + mount=MountType.RIGHT, deck_point=DeckPoint(x=333, y=444, z=555) + ) + + def test_handles_load_pipette(subject: PipetteStore) -> None: """It should add the pipette data to the state.""" command = create_load_pipette_command( @@ -332,289 +434,6 @@ def test_blow_out_clears_volume( assert subject.state.aspirated_volume_by_id["pipette-id"] is None -@pytest.mark.parametrize( - ("action", "expected_location"), - ( - ( - SucceedCommandAction( - command=create_aspirate_command( - pipette_id="pipette-id", - labware_id="aspirate-labware-id", - well_name="aspirate-well-name", - volume=1337, - flow_rate=1.23, - ), - private_result=None, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="aspirate-labware-id", - well_name="aspirate-well-name", - ), - ), - ( - FailCommandAction( - running_command=cmd.Aspirate( - params=cmd.AspirateParams( - pipetteId="pipette-id", - labwareId="aspirate-labware-id", - wellName="aspirate-well-name", - volume=99999, - flowRate=1.23, - ), - id="command-id", - key="command-key", - createdAt=datetime.now(), - status=cmd.CommandStatus.RUNNING, - ), - error=DefinedErrorData( - public=OverpressureError( - id="error-id", - createdAt=datetime.now(), - errorInfo={"retryLocation": (0, 0, 0)}, - ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=0, y=0, z=0) - ), - ), - command_id="command-id", - error_id="error-id", - failed_at=datetime.now(), - notes=[], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="aspirate-labware-id", - well_name="aspirate-well-name", - ), - ), - ( - SucceedCommandAction( - command=create_dispense_command( - pipette_id="pipette-id", - labware_id="dispense-labware-id", - well_name="dispense-well-name", - volume=1337, - flow_rate=1.23, - ), - private_result=None, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="dispense-labware-id", - well_name="dispense-well-name", - ), - ), - ( - SucceedCommandAction( - command=create_pick_up_tip_command( - pipette_id="pipette-id", - labware_id="pick-up-tip-labware-id", - well_name="pick-up-tip-well-name", - ), - private_result=None, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="pick-up-tip-labware-id", - well_name="pick-up-tip-well-name", - ), - ), - ( - SucceedCommandAction( - command=create_drop_tip_command( - pipette_id="pipette-id", - labware_id="drop-tip-labware-id", - well_name="drop-tip-well-name", - ), - private_result=None, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="drop-tip-labware-id", - well_name="drop-tip-well-name", - ), - ), - ( - SucceedCommandAction( - command=create_move_to_well_command( - pipette_id="pipette-id", - labware_id="move-to-well-labware-id", - well_name="move-to-well-well-name", - ), - private_result=None, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="move-to-well-labware-id", - well_name="move-to-well-well-name", - ), - ), - ( - SucceedCommandAction( - command=create_blow_out_command( - pipette_id="pipette-id", - labware_id="move-to-well-labware-id", - well_name="move-to-well-well-name", - flow_rate=1.23, - ), - private_result=None, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="move-to-well-labware-id", - well_name="move-to-well-well-name", - ), - ), - ( - FailCommandAction( - running_command=cmd.Dispense( - params=cmd.DispenseParams( - pipetteId="pipette-id", - labwareId="dispense-labware-id", - wellName="dispense-well-name", - volume=50, - flowRate=1.23, - ), - id="command-id", - key="command-key", - createdAt=datetime.now(), - status=cmd.CommandStatus.RUNNING, - ), - error=DefinedErrorData( - public=OverpressureError( - id="error-id", - createdAt=datetime.now(), - errorInfo={"retryLocation": (0, 0, 0)}, - ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=0, y=0, z=0) - ), - ), - command_id="command-id", - error_id="error-id", - failed_at=datetime.now(), - notes=[], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="dispense-labware-id", - well_name="dispense-well-name", - ), - ), - # liquidProbe and tryLiquidProbe succeeding and with overpressure error - ( - SucceedCommandAction( - command=cmd.LiquidProbe( - id="command-id", - createdAt=datetime.now(), - startedAt=datetime.now(), - completedAt=datetime.now(), - key="command-key", - status=cmd.CommandStatus.SUCCEEDED, - params=cmd.LiquidProbeParams( - labwareId="liquid-probe-labware-id", - wellName="liquid-probe-well-name", - pipetteId="pipette-id", - ), - result=cmd.LiquidProbeResult( - position=DeckPoint(x=0, y=0, z=0), z_position=0 - ), - ), - private_result=None, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="liquid-probe-labware-id", - well_name="liquid-probe-well-name", - ), - ), - ( - FailCommandAction( - running_command=cmd.LiquidProbe( - id="command-id", - createdAt=datetime.now(), - startedAt=datetime.now(), - key="command-key", - status=cmd.CommandStatus.RUNNING, - params=cmd.LiquidProbeParams( - labwareId="liquid-probe-labware-id", - wellName="liquid-probe-well-name", - pipetteId="pipette-id", - ), - ), - error=DefinedErrorData( - public=LiquidNotFoundError( - id="error-id", - createdAt=datetime.now(), - ), - private=LiquidNotFoundErrorInternalData( - position=DeckPoint(x=0, y=0, z=0) - ), - ), - command_id="command-id", - error_id="error-id", - failed_at=datetime.now(), - notes=[], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="liquid-probe-labware-id", - well_name="liquid-probe-well-name", - ), - ), - ( - SucceedCommandAction( - command=cmd.TryLiquidProbe( - id="command-id", - createdAt=datetime.now(), - startedAt=datetime.now(), - completedAt=datetime.now(), - key="command-key", - status=cmd.CommandStatus.SUCCEEDED, - params=cmd.TryLiquidProbeParams( - labwareId="try-liquid-probe-labware-id", - wellName="try-liquid-probe-well-name", - pipetteId="pipette-id", - ), - result=cmd.TryLiquidProbeResult( - position=DeckPoint(x=0, y=0, z=0), - z_position=0, - ), - ), - private_result=None, - ), - CurrentWell( - pipette_id="pipette-id", - labware_id="try-liquid-probe-labware-id", - well_name="try-liquid-probe-well-name", - ), - ), - ), -) -def test_movement_commands_update_current_well( - action: Union[SucceedCommandAction, FailCommandAction], - expected_location: CurrentWell, - subject: PipetteStore, -) -> None: - """It should save the last used pipette, labware, and well for movement commands.""" - load_pipette_command = create_load_pipette_command( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - - subject.handle_action( - SucceedCommandAction(private_result=None, command=load_pipette_command) - ) - subject.handle_action(action) - - assert subject.state.current_location == expected_location - - @pytest.mark.parametrize( "command", [ @@ -685,19 +504,20 @@ def test_movement_commands_without_well_clear_current_well( pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - move_command = create_move_to_well_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - ) - subject.handle_action( SucceedCommandAction(private_result=None, command=load_pipette_command) ) + subject.handle_action( - SucceedCommandAction(private_result=None, command=move_command) + _create_move_to_well_action( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + deck_point=DeckPoint(x=1, y=2, z=3), + ) ) - subject.handle_action(SucceedCommandAction(private_result=None, command=command)) + + subject.handle_action(SucceedCommandAction(command=command, private_result=None)) assert subject.state.current_location is None @@ -737,19 +557,19 @@ def test_heater_shaker_command_without_movement( pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - move_command = create_move_to_well_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - destination=DeckPoint(x=1, y=2, z=3), - ) - subject.handle_action( SucceedCommandAction(private_result=None, command=load_pipette_command) ) + subject.handle_action( - SucceedCommandAction(private_result=None, command=move_command) + _create_move_to_well_action( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + deck_point=DeckPoint(x=1, y=2, z=3), + ) ) + subject.handle_action(SucceedCommandAction(private_result=None, command=command)) assert subject.state.current_location == CurrentWell( @@ -849,17 +669,17 @@ def test_move_labware_clears_current_well( pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - move_to_well_command = create_move_to_well_command( - pipette_id="pipette-id", - labware_id="matching-labware-id", - well_name="well-name", - ) - subject.handle_action( SucceedCommandAction(private_result=None, command=load_pipette_command) ) + subject.handle_action( - SucceedCommandAction(private_result=None, command=move_to_well_command) + _create_move_to_well_action( + pipette_id="pipette-id", + labware_id="matching-labware-id", + well_name="well-name", + deck_point=DeckPoint(x=1, y=2, z=3), + ) ) subject.handle_action( @@ -956,105 +776,6 @@ def test_add_pipette_config( @pytest.mark.parametrize( "action", ( - SucceedCommandAction( - command=create_aspirate_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - volume=1337, - flow_rate=1.23, - destination=DeckPoint(x=11, y=22, z=33), - ), - private_result=None, - ), - FailCommandAction( - running_command=cmd.Aspirate( - params=cmd.AspirateParams( - pipetteId="pipette-id", - labwareId="labware-id", - wellName="well-name", - volume=99999, - flowRate=1.23, - ), - id="command-id", - key="command-key", - createdAt=datetime.now(), - status=cmd.CommandStatus.RUNNING, - ), - error=DefinedErrorData( - public=OverpressureError( - id="error-id", - detail="error-detail", - createdAt=datetime.now(), - errorInfo={"retryLocation": (11, 22, 33)}, - ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=11, y=22, z=33) - ), - ), - command_id="command-id", - error_id="error-id", - failed_at=datetime.now(), - notes=[], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ), - SucceedCommandAction( - command=create_dispense_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - volume=1337, - flow_rate=1.23, - destination=DeckPoint(x=11, y=22, z=33), - ), - private_result=None, - ), - SucceedCommandAction( - command=create_blow_out_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - flow_rate=1.23, - destination=DeckPoint(x=11, y=22, z=33), - ), - private_result=None, - ), - SucceedCommandAction( - command=create_pick_up_tip_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - destination=DeckPoint(x=11, y=22, z=33), - ), - private_result=None, - ), - SucceedCommandAction( - command=create_drop_tip_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - destination=DeckPoint(x=11, y=22, z=33), - ), - private_result=None, - ), - SucceedCommandAction( - command=create_touch_tip_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - destination=DeckPoint(x=11, y=22, z=33), - ), - private_result=None, - ), - SucceedCommandAction( - command=create_move_to_well_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - destination=DeckPoint(x=11, y=22, z=33), - ), - private_result=None, - ), SucceedCommandAction( command=create_move_to_coordinates_command( pipette_id="pipette-id", @@ -1069,95 +790,6 @@ def test_add_pipette_config( ), private_result=None, ), - FailCommandAction( - running_command=cmd.Dispense( - params=cmd.DispenseParams( - pipetteId="pipette-id", - labwareId="labware-id", - wellName="well-name", - volume=125, - flowRate=1.23, - ), - id="command-id", - key="command-key", - createdAt=datetime.now(), - status=cmd.CommandStatus.RUNNING, - ), - error=DefinedErrorData( - public=OverpressureError( - id="error-id", - detail="error-detail", - createdAt=datetime.now(), - errorInfo={"retryLocation": (11, 22, 33)}, - ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=11, y=22, z=33) - ), - ), - command_id="command-id", - error_id="error-id", - failed_at=datetime.now(), - notes=[], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ), - FailCommandAction( - running_command=cmd.AspirateInPlace( - params=cmd.AspirateInPlaceParams( - pipetteId="pipette-id", - volume=125, - flowRate=1.23, - ), - id="command-id", - key="command-key", - createdAt=datetime.now(), - status=cmd.CommandStatus.RUNNING, - ), - error=DefinedErrorData( - public=OverpressureError( - id="error-id", - detail="error-detail", - createdAt=datetime.now(), - errorInfo={"retryLocation": (11, 22, 33)}, - ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=11, y=22, z=33) - ), - ), - command_id="command-id", - error_id="error-id", - failed_at=datetime.now(), - notes=[], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ), - FailCommandAction( - running_command=cmd.DispenseInPlace( - params=cmd.DispenseInPlaceParams( - pipetteId="pipette-id", - volume=125, - flowRate=1.23, - ), - id="command-id", - key="command-key", - createdAt=datetime.now(), - status=cmd.CommandStatus.RUNNING, - ), - error=DefinedErrorData( - public=OverpressureError( - id="error-id", - detail="error-detail", - createdAt=datetime.now(), - errorInfo={"retryLocation": (11, 22, 33)}, - ), - private=OverpressureErrorInternalData( - position=DeckPoint(x=11, y=22, z=33) - ), - ), - command_id="command-id", - error_id="error-id", - failed_at=datetime.now(), - notes=[], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ), ), ) def test_movement_commands_update_deck_point( @@ -1239,32 +871,28 @@ def test_homing_commands_clear_deck_point( command: cmd.Command, subject: PipetteStore, ) -> None: - """It should save the last used pipette, labware, and well for movement commands.""" + """Commands that homed the robot should clear the deck point.""" load_pipette_command = create_load_pipette_command( pipette_id="pipette-id", pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - move_command = create_move_to_well_command( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="well-name", - destination=DeckPoint(x=1, y=2, z=3), - ) - subject.handle_action( SucceedCommandAction(private_result=None, command=load_pipette_command) ) subject.handle_action( - SucceedCommandAction(private_result=None, command=move_command) + _create_move_to_well_action( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + deck_point=DeckPoint(x=1, y=2, z=3), + ) ) - assert subject.state.current_deck_point == CurrentDeckPoint( mount=MountType.LEFT, deck_point=DeckPoint(x=1, y=2, z=3) ) subject.handle_action(SucceedCommandAction(private_result=None, command=command)) - assert subject.state.current_deck_point == CurrentDeckPoint( mount=None, deck_point=None ) diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index 6cd24564795..9cebd0a80d2 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -9,7 +9,8 @@ from opentrons.util.change_notifier import ChangeNotifier from opentrons.protocol_engine.actions import PlayAction -from opentrons.protocol_engine.state import State, StateStore, Config +from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.state.state import State, StateStore from opentrons.protocol_engine.types import DeckType @@ -48,6 +49,12 @@ def placeholder_error_recovery_policy(*args: object, **kwargs: object) -> Any: "robotType": "OT-2 Standard", "models": ["OT-2 Standard", "OT-2 Refresh"], "extents": [446.75, 347.5, 0.0], + "paddingOffsets": { + "rear": -35.91, + "front": 31.89, + "leftSide": 0, + "rightSide": 0, + }, "mountOffsets": {"left": [-34.0, 0.0, 0.0], "right": [0.0, 0.0, 0.0]}, }, deck_fixed_labware=[], diff --git a/api/tests/opentrons/protocol_engine/state/test_well_store.py b/api/tests/opentrons/protocol_engine/state/test_well_store.py new file mode 100644 index 00000000000..325021a9942 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_well_store.py @@ -0,0 +1,28 @@ +"""Well state store tests.""" +import pytest +from opentrons.protocol_engine.state.wells import WellStore +from opentrons.protocol_engine.actions.actions import SucceedCommandAction + +from .command_fixtures import create_liquid_probe_command + + +@pytest.fixture +def subject() -> WellStore: + """Well store test subject.""" + return WellStore() + + +def test_handles_liquid_probe_success(subject: WellStore) -> None: + """It should add the well to the state after a successful liquid probe.""" + labware_id = "labware-id" + well_name = "well-name" + + liquid_probe = create_liquid_probe_command() + + subject.handle_action( + SucceedCommandAction(private_result=None, command=liquid_probe) + ) + + assert len(subject.state.measured_liquid_heights) == 1 + + assert subject.state.measured_liquid_heights[labware_id][well_name].height == 0.5 diff --git a/api/tests/opentrons/protocol_engine/state/test_well_view.py b/api/tests/opentrons/protocol_engine/state/test_well_view.py new file mode 100644 index 00000000000..3bd86e9dcb9 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_well_view.py @@ -0,0 +1,51 @@ +"""Well view tests.""" +from datetime import datetime +from opentrons.protocol_engine.types import LiquidHeightInfo +import pytest +from opentrons.protocol_engine.state.wells import WellState, WellView + + +@pytest.fixture +def subject() -> WellView: + """Get a well view test subject.""" + labware_id = "labware-id" + well_name = "well-name" + height_info = LiquidHeightInfo(height=0.5, last_measured=datetime.now()) + state = WellState(measured_liquid_heights={labware_id: {well_name: height_info}}) + + return WellView(state) + + +def test_get_all(subject: WellView) -> None: + """Should return a list of well heights.""" + assert subject.get_all()[0].height == 0.5 + + +def test_get_last_measured_liquid_height(subject: WellView) -> None: + """Should return the height of a single well correctly for valid wells.""" + labware_id = "labware-id" + well_name = "well-name" + + invalid_labware_id = "invalid-labware-id" + invalid_well_name = "invalid-well-name" + + assert ( + subject.get_last_measured_liquid_height(invalid_labware_id, invalid_well_name) + is None + ) + assert subject.get_last_measured_liquid_height(labware_id, well_name) == 0.5 + + +def test_has_measured_liquid_height(subject: WellView) -> None: + """Should return True for measured wells and False for ones that have no measurements.""" + labware_id = "labware-id" + well_name = "well-name" + + invalid_labware_id = "invalid-labware-id" + invalid_well_name = "invalid-well-name" + + assert ( + subject.has_measured_liquid_height(invalid_labware_id, invalid_well_name) + is False + ) + assert subject.has_measured_liquid_height(labware_id, well_name) is True diff --git a/api/tests/opentrons/protocol_engine/test_plugins.py b/api/tests/opentrons/protocol_engine/test_plugins.py index 0da44ab62bc..5efe4736cfa 100644 --- a/api/tests/opentrons/protocol_engine/test_plugins.py +++ b/api/tests/opentrons/protocol_engine/test_plugins.py @@ -3,7 +3,7 @@ from decoy import Decoy from datetime import datetime -from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.plugins import AbstractPlugin, PluginStarter from opentrons.protocol_engine.actions import ActionDispatcher, Action, PlayAction diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 2669640e649..71e23cfe715 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -39,7 +39,8 @@ DoorWatcher, ) from opentrons.protocol_engine.resources import ModelUtils, ModuleDataProvider -from opentrons.protocol_engine.state import Config, StateStore +from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.plugins import AbstractPlugin, PluginStarter from opentrons.protocol_engine.errors import ProtocolCommandFailedError, ErrorOccurrence diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index c18943e8f5c..d7c82ca7475 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -13,8 +13,7 @@ Group, Metadata1, WellDefinition, - BoundedSection, - RectangularCrossSection, + RectangularBoundedSection, InnerWellGeometry, SphericalSegment, ) @@ -692,20 +691,16 @@ def _load_labware_definition_data() -> LabwareDefinition: innerLabwareGeometry={ "welldefinition1111": InnerWellGeometry( frusta=[ - BoundedSection( - geometry=RectangularCrossSection( - shape="rectangular", - xDimension=7.6, - yDimension=8.5, - ), + RectangularBoundedSection( + shape="rectangular", + xDimension=7.6, + yDimension=8.5, topHeight=45, ), - BoundedSection( - geometry=RectangularCrossSection( - shape="rectangular", - xDimension=5.6, - yDimension=6.5, - ), + RectangularBoundedSection( + shape="rectangular", + xDimension=5.6, + yDimension=6.5, topHeight=20, ), ], diff --git a/api/tests/opentrons/protocol_runner/test_run_orchestrator.py b/api/tests/opentrons/protocol_runner/test_run_orchestrator.py index 6e1c04949f8..ff0938a2e6d 100644 --- a/api/tests/opentrons/protocol_runner/test_run_orchestrator.py +++ b/api/tests/opentrons/protocol_runner/test_run_orchestrator.py @@ -10,7 +10,7 @@ from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy from opentrons.protocol_engine.errors import RunStoppedError -from opentrons.protocol_engine.state import StateStore +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocols.api_support.types import APIVersion from opentrons.protocol_engine import ProtocolEngine from opentrons.protocol_engine.types import PostRunHardwareState diff --git a/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py b/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py index 81bffd0028e..f0bf7d89f32 100644 --- a/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py +++ b/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py @@ -1,3 +1,4 @@ +from typing import List, Tuple import pytest import platform from decoy import Decoy @@ -44,6 +45,42 @@ def csv_file_different_delimiter() -> bytes: return b"x:y:z\na,:1,:2\nb,:3,:4\nc,:5,:6" +@pytest.fixture +def csv_file_basic_trailing_empty() -> Tuple[bytes, List[List[str]]]: + """A basic CSV file with quotes around strings and a trailing newline.""" + return ( + b'"x","y","z"\n"a",1,2\n"b",3,4\n"c",5,6\n', + [["x", "y", "z"], ["a", "1", "2"], ["b", "3", "4"], ["c", "5", "6"]], + ) + + +@pytest.fixture +def csv_file_basic_three_trailing_empty() -> Tuple[bytes, List[List[str]]]: + """A basic CSV file with quotes around strings and three trailing newlines.""" + return ( + b'"x","y","z"\n"a",1,2\n"b",3,4\n"c",5,6\n\n\n', + [["x", "y", "z"], ["a", "1", "2"], ["b", "3", "4"], ["c", "5", "6"]], + ) + + +@pytest.fixture +def csv_file_empty_row_and_trailing_empty() -> Tuple[bytes, List[List[str]]]: + """A basic CSV file with quotes around strings, an empty row, and a trailing newline.""" + return ( + b'"x","y","z"\n\n"b",3,4\n"c",5,6\n', + [["x", "y", "z"], [], ["b", "3", "4"], ["c", "5", "6"]], + ) + + +@pytest.fixture +def csv_file_windows_empty_row_trailing_empty() -> Tuple[bytes, List[List[str]]]: + """A basic CSV file with quotes around strings, Windows-style newlines, an empty row, and a trailing newline.""" + return ( + b'"x","y","z"\r\n\r\n"b",3,4\r\n"c",5,6\r\n', + [["x", "y", "z"], [], ["b", "3", "4"], ["c", "5", "6"]], + ) + + def test_csv_parameter( decoy: Decoy, api_version: APIVersion, csv_file_basic: bytes ) -> None: @@ -125,3 +162,35 @@ def test_csv_parameter_dont_detect_dialect( assert rows[0] == ["x", ' "y"', ' "z"'] assert rows[1] == ["a", " 1", " 2"] + + +@pytest.mark.parametrize( + "csv_file_fixture", + [ + "csv_file_basic_trailing_empty", + "csv_file_basic_three_trailing_empty", + "csv_file_empty_row_and_trailing_empty", + "csv_file_windows_empty_row_trailing_empty", + ], +) +def test_csv_parameter_trailing_empties( + decoy: Decoy, + api_version: APIVersion, + request: pytest.FixtureRequest, + csv_file_fixture: str, +) -> None: + """It should load the rows as all strings. Empty rows are allowed in the middle of the data but all trailing empty rows are removed.""" + # Get the fixture value + csv_file: bytes + expected_output: List[List[str]] + csv_file, expected_output = request.getfixturevalue(csv_file_fixture) + + subject = CSVParameter(csv_file, api_version) + parsed_csv = subject.parse_as_csv() + + assert ( + parsed_csv == expected_output + ), f"Expected {expected_output}, but got {parsed_csv}" + assert len(parsed_csv) == len( + expected_output + ), f"Expected {len(expected_output)} rows, but got {len(parsed_csv)}" diff --git a/app-shell-odd/package.json b/app-shell-odd/package.json index e080060ca7c..624486e5332 100644 --- a/app-shell-odd/package.json +++ b/app-shell-odd/package.json @@ -55,7 +55,7 @@ "node-fetch": "2.6.7", "node-stream-zip": "1.8.2", "pump": "3.0.0", - "semver": "5.5.0", + "semver": "5.7.2", "tempy": "1.0.1", "uuid": "3.2.1", "winston": "3.1.0", diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index fa21418bea2..5ab9009bebc 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -25,6 +25,11 @@ Welcome to the v8.0.0 release of the Opentrons App! - Lists of liquids now separately show the total volume and per-well volume (when it is the same in each well containing that liquid). - Improved instructions for what to do when a Flex protocol completes or is canceled with liquid-filled tips attached to the pipette. +### Known Issues + +- Labware offsets can't be applied to protocols that require selecting a CSV file as a runtime parameter value. Write the protocol in such a way that it passes analysis with or without the CSV file, or run Labware Position Check after confirming parameter values. +- Error recovery can't perform partial tip pickup, because it doesn't account for the pipette nozzle configuration of 8- and 96-channel pipettes. If a recovery step requires partial tip pickup, cancel the protocol instead. + --- ## Opentrons App Changes in 7.5.0 diff --git a/app-shell/package.json b/app-shell/package.json index e93babb3342..1a1461da837 100644 --- a/app-shell/package.json +++ b/app-shell/package.json @@ -63,7 +63,7 @@ "node-fetch": "2.6.7", "node-stream-zip": "1.8.2", "pump": "3.0.0", - "semver": "5.5.0", + "semver": "5.7.2", "serialport": "10.5.0", "tempy": "1.0.1", "usb": "^2.11.0", diff --git a/app-shell/src/system-info/usb-devices.ts b/app-shell/src/system-info/usb-devices.ts index 660f606fd03..de36ba6a877 100644 --- a/app-shell/src/system-info/usb-devices.ts +++ b/app-shell/src/system-info/usb-devices.ts @@ -133,10 +133,15 @@ function upstreamDeviceFromUsbDeviceWinAPI( const parsePoshJsonOutputToWmiObjectArray = ( dump: string ): WmiObject[] => { - if (dump[0] === '[') { - return JSON.parse(dump) as WmiObject[] - } else { - return [JSON.parse(dump) as WmiObject] + try { + if (dump[0] === '[') { + return JSON.parse(dump) as WmiObject[] + } else { + return [JSON.parse(dump) as WmiObject] + } + } catch (e: any) { + log.error(`Failed to parse posh json output: ${dump}`) + throw e } } if (dump.stderr !== '') { diff --git a/app-shell/src/usb.ts b/app-shell/src/usb.ts index 276c537f062..cf551653674 100644 --- a/app-shell/src/usb.ts +++ b/app-shell/src/usb.ts @@ -94,6 +94,19 @@ function reconstructFormData(ipcSafeFormData: IPCSafeFormData): FormData { return result } +const cloneError = (e: any): Record => + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.entries(axios.isAxiosError(e) ? e.toJSON() : e).reduce< + Record + >((acc, [k, v]) => { + try { + acc[k] = structuredClone(v) + return acc + } catch (e) { + return acc + } + }, {}) + async function usbListener( _event: IpcMainInvokeEvent, config: AxiosRequestConfig @@ -114,21 +127,24 @@ async function usbListener( const usbHttpAgent = getSerialPortHttpAgent() try { + usbLog.silly(`${config.method} ${config.url} timeout=${config.timeout}`) const response = await axios.request({ httpAgent: usbHttpAgent, ...config, data, headers: { ...config.headers, ...formHeaders }, }) + usbLog.silly(`${config.method} ${config.url} resolved ok`) return { - error: false, + error: null, data: response.data, status: response.status, statusText: response.statusText, } - } catch (e) { - if (e instanceof Error) { - console.log(`axios request error ${e?.message ?? 'unknown'}`) + } catch (e: any) { + usbLog.info(`${config.method} ${config.url} failed: ${e}`) + return { + error: cloneError(e), } } } diff --git a/app/Makefile b/app/Makefile index 51305c9b3b0..b11fed0b2b3 100644 --- a/app/Makefile +++ b/app/Makefile @@ -21,7 +21,7 @@ discovery_client_dir := ../discovery-client # These variables can be overriden when make is invoked to customize the # behavior of jest. For instance, -# make test tests=src/pages/Labware/__tests__/hooks.test.tsx would run only the +# make test tests=src/pages/Desktop/Labware/__tests__/hooks.test.tsx would run only the # specified test tests ?= cov_opts ?= --coverage=true diff --git a/app/package.json b/app/package.json index ad78d15a779..034e5dd7cec 100644 --- a/app/package.json +++ b/app/package.json @@ -62,7 +62,7 @@ "redux-thunk": "2.3.0", "reselect": "4.0.0", "rxjs": "^6.5.1", - "semver": "5.5.0", + "semver": "5.7.2", "styled-components": "5.3.6", "typeface-open-sans": "0.0.75", "uuid": "3.2.1" diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index 9bcae5fd201..dfa4c2bb544 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -16,15 +16,15 @@ import { i18n } from '../i18n' import { Alerts } from '../organisms/Alerts' import { Breadcrumbs } from '../organisms/Breadcrumbs' import { ToasterOven } from '../organisms/ToasterOven' -import { CalibrationDashboard } from '../pages/Devices/CalibrationDashboard' -import { DeviceDetails } from '../pages/Devices/DeviceDetails' -import { DevicesLanding } from '../pages/Devices/DevicesLanding' -import { ProtocolRunDetails } from '../pages/Devices/ProtocolRunDetails' -import { RobotSettings } from '../pages/Devices/RobotSettings' -import { ProtocolsLanding } from '../pages/Protocols/ProtocolsLanding' -import { ProtocolDetails } from '../pages/Protocols/ProtocolDetails' -import { AppSettings } from '../pages/AppSettings' -import { Labware } from '../pages/Labware' +import { CalibrationDashboard } from '../pages/Desktop/Devices/CalibrationDashboard' +import { DeviceDetails } from '../pages/Desktop/Devices/DeviceDetails' +import { DevicesLanding } from '../pages/Desktop/Devices/DevicesLanding' +import { ProtocolRunDetails } from '../pages/Desktop/Devices/ProtocolRunDetails' +import { RobotSettings } from '../pages/Desktop/Devices/RobotSettings' +import { ProtocolsLanding } from '../pages/Desktop/Protocols/ProtocolsLanding' +import { ProtocolDetails } from '../pages/Desktop/Protocols/ProtocolDetails' +import { AppSettings } from '../pages/Desktop/AppSettings' +import { Labware } from '../pages/Desktop/Labware' import { useSoftwareUpdatePoll } from './hooks' import { Navbar } from './Navbar' import { EstopTakeover, EmergencyStopContext } from '../organisms/EmergencyStop' @@ -32,11 +32,11 @@ import { IncompatibleModuleTakeover } from '../organisms/IncompatibleModule' import { OPENTRONS_USB } from '../redux/discovery' import { appShellRequestor } from '../redux/shell/remote' import { useRobot, useIsFlex } from '../organisms/Devices/hooks' -import { ProtocolTimeline } from '../pages/Protocols/ProtocolDetails/ProtocolTimeline' +import { ProtocolTimeline } from '../pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline' import { PortalRoot as ModalPortalRoot } from './portal' import { DesktopAppFallback } from './DesktopAppFallback' -import type { RouteProps, DesktopRouteParams } from './types' +import type { RouteProps } from './types' export const DesktopApp = (): JSX.Element => { useSoftwareUpdatePoll() @@ -158,11 +158,10 @@ export const DesktopApp = (): JSX.Element => { } function RobotControlTakeover(): JSX.Element | null { - const deviceRouteMatch = useMatch('/devices/:robotName') - const params = deviceRouteMatch?.params as DesktopRouteParams - const robotName = params?.robotName + const deviceRouteMatch = useMatch('/devices/:robotName/*') + const params = deviceRouteMatch?.params + const robotName = params?.robotName ?? null const robot = useRobot(robotName) - if (deviceRouteMatch == null || robot == null || robotName == null) return null diff --git a/app/src/App/ODDTopLevelRedirects/constants.ts b/app/src/App/ODDTopLevelRedirects/constants.ts new file mode 100644 index 00000000000..002bea95326 --- /dev/null +++ b/app/src/App/ODDTopLevelRedirects/constants.ts @@ -0,0 +1 @@ +export const CURRENT_RUN_POLL = 5000 diff --git a/app/src/App/ODDTopLevelRedirects/hooks/__tests__/useCurrentRunRoute.test.ts b/app/src/App/ODDTopLevelRedirects/hooks/__tests__/useCurrentRunRoute.test.ts new file mode 100644 index 00000000000..129632ffe30 --- /dev/null +++ b/app/src/App/ODDTopLevelRedirects/hooks/__tests__/useCurrentRunRoute.test.ts @@ -0,0 +1,146 @@ +import { renderHook } from '@testing-library/react' +import { describe, it, expect, vi } from 'vitest' +import { useCurrentRunRoute } from '../useCurrentRunRoute' +import { useNotifyRunQuery } from '../../../../resources/runs' +import { + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_FAILED, + RUN_STATUS_IDLE, + RUN_STATUS_STOPPED, + RUN_STATUS_SUCCEEDED, +} from '@opentrons/api-client' + +vi.mock('../../../../resources/runs') + +const MOCK_RUN_ID = 'MOCK_RUN_ID' + +describe('useCurrentRunRoute', () => { + it('returns null when the run record is null', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: null, + isFetching: false, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBeNull() + }) + + it('returns null when isFetching is true', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { data: { startedAt: '123' } }, + isFetching: true, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBeNull() + }) + + it('returns the summary route for a run with succeeded status', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { + data: { + id: MOCK_RUN_ID, + status: RUN_STATUS_SUCCEEDED, + startedAt: '123', + }, + }, + isFetching: false, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBe(`/runs/${MOCK_RUN_ID}/summary`) + }) + + it('returns the summary route for a started run that has a stopped status', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { + data: { + id: MOCK_RUN_ID, + status: RUN_STATUS_STOPPED, + startedAt: '123', + }, + }, + isFetching: false, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBe(`/runs/${MOCK_RUN_ID}/summary`) + }) + + it('returns summary route for a run with failed status', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { + data: { id: MOCK_RUN_ID, status: RUN_STATUS_FAILED, startedAt: '123' }, + }, + isFetching: false, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBe(`/runs/${MOCK_RUN_ID}/summary`) + }) + + it('returns the setup route for a run with an idle status', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { + data: { id: MOCK_RUN_ID, status: RUN_STATUS_IDLE, startedAt: null }, + }, + isFetching: false, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBe(`/runs/${MOCK_RUN_ID}/setup`) + }) + + it('returns the setup route for a "blocked by open door" run that has not been started yet', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { + data: { + id: MOCK_RUN_ID, + status: RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + startedAt: null, + }, + }, + isFetching: false, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBe(`/runs/${MOCK_RUN_ID}/setup`) + }) + + it('returns run route for a started run', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { + data: { + id: MOCK_RUN_ID, + status: RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + startedAt: '123', + }, + }, + isFetching: false, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBe(`/runs/${MOCK_RUN_ID}/run`) + }) + + it('returns null for cancelled run before starting', () => { + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { + data: { id: MOCK_RUN_ID, status: RUN_STATUS_STOPPED, startedAt: null }, + }, + isFetching: false, + } as any) + + const { result } = renderHook(() => useCurrentRunRoute(MOCK_RUN_ID)) + + expect(result.current).toBeNull() + }) +}) diff --git a/app/src/App/ODDTopLevelRedirects/hooks/index.ts b/app/src/App/ODDTopLevelRedirects/hooks/index.ts new file mode 100644 index 00000000000..d93d3540a9c --- /dev/null +++ b/app/src/App/ODDTopLevelRedirects/hooks/index.ts @@ -0,0 +1 @@ +export { useCurrentRunRoute } from './useCurrentRunRoute' diff --git a/app/src/App/ODDTopLevelRedirects/hooks/useCurrentRunRoute.ts b/app/src/App/ODDTopLevelRedirects/hooks/useCurrentRunRoute.ts new file mode 100644 index 00000000000..47a9d5e670b --- /dev/null +++ b/app/src/App/ODDTopLevelRedirects/hooks/useCurrentRunRoute.ts @@ -0,0 +1,42 @@ +import { + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_FAILED, + RUN_STATUS_IDLE, + RUN_STATUS_STOPPED, + RUN_STATUS_SUCCEEDED, +} from '@opentrons/api-client' + +import { useNotifyRunQuery } from '../../../resources/runs' +import { CURRENT_RUN_POLL } from '../constants' + +// Returns the route to which React Router should navigate, if any. +export function useCurrentRunRoute(currentRunId: string): string | null { + const { data: runRecord, isFetching } = useNotifyRunQuery(currentRunId, { + refetchInterval: CURRENT_RUN_POLL, + }) + + // grabbing run id off of the run query to have all routing info come from one source of truth + const runId = runRecord?.data.id + const hasRunStarted = runRecord?.data.startedAt != null + const runStatus = runRecord?.data.status + + if (isFetching) { + return null + } else if ( + runStatus === RUN_STATUS_SUCCEEDED || + (runStatus === RUN_STATUS_STOPPED && hasRunStarted) || + runStatus === RUN_STATUS_FAILED + ) { + return `/runs/${runId}/summary` + } else if ( + runStatus === RUN_STATUS_IDLE || + (!hasRunStarted && runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR) + ) { + return `/runs/${runId}/setup` + } else if (hasRunStarted) { + return `/runs/${runId}/run` + } else { + // includes runs cancelled before starting and runs not yet started + return null + } +} diff --git a/app/src/App/ODDTopLevelRedirects/index.tsx b/app/src/App/ODDTopLevelRedirects/index.tsx new file mode 100644 index 00000000000..4ecfc50e618 --- /dev/null +++ b/app/src/App/ODDTopLevelRedirects/index.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import { Navigate, Route, Routes } from 'react-router-dom' + +import { useCurrentRunId } from '../../resources/runs' +import { CURRENT_RUN_POLL } from './constants' +import { useCurrentRunRoute } from './hooks' + +export function ODDTopLevelRedirects(): JSX.Element | null { + const currentRunId = useCurrentRunId({ refetchInterval: CURRENT_RUN_POLL }) + + return currentRunId != null ? ( + + ) : null +} + +function CurrentRunRoute({ + currentRunId, +}: { + currentRunId: string +}): JSX.Element | null { + const currentRunRoute = useCurrentRunRoute(currentRunId) + + return currentRunRoute != null ? ( + + } /> + + ) : null +} diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 77aceabce20..62f29af637d 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -1,14 +1,14 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { Routes, Route, Navigate } from 'react-router-dom' +import { Navigate, Route, Routes } from 'react-router-dom' import { css } from 'styled-components' import { ErrorBoundary } from 'react-error-boundary' import { Box, - POSITION_RELATIVE, COLORS, OVERFLOW_AUTO, + POSITION_RELATIVE, useIdle, useScrolling, } from '@opentrons/components' @@ -22,38 +22,35 @@ import { MaintenanceRunTakeover } from '../organisms/TakeoverModal' import { FirmwareUpdateTakeover } from '../organisms/FirmwareUpdateModal/FirmwareUpdateTakeover' import { IncompatibleModuleTakeover } from '../organisms/IncompatibleModule' import { EstopTakeover } from '../organisms/EmergencyStop' -import { ConnectViaEthernet } from '../pages/ConnectViaEthernet' -import { ConnectViaUSB } from '../pages/ConnectViaUSB' -import { ConnectViaWifi } from '../pages/ConnectViaWifi' -import { EmergencyStop } from '../pages/EmergencyStop' -import { NameRobot } from '../pages/NameRobot' -import { NetworkSetupMenu } from '../pages/NetworkSetupMenu' -import { ProtocolSetup } from '../pages/ProtocolSetup' -import { RobotDashboard } from '../pages/RobotDashboard' -import { RobotSettingsDashboard } from '../pages/RobotSettingsDashboard' -import { ProtocolDashboard } from '../pages/ProtocolDashboard' -import { ProtocolDetails } from '../pages/ProtocolDetails' +import { ConnectViaEthernet } from '../pages/ODD/ConnectViaEthernet' +import { ConnectViaUSB } from '../pages/ODD/ConnectViaUSB' +import { ConnectViaWifi } from '../pages/ODD/ConnectViaWifi' +import { EmergencyStop } from '../pages/ODD/EmergencyStop' +import { NameRobot } from '../pages/ODD/NameRobot' +import { NetworkSetupMenu } from '../pages/ODD/NetworkSetupMenu' +import { ProtocolSetup } from '../pages/ODD/ProtocolSetup' +import { RobotDashboard } from '../pages/ODD/RobotDashboard' +import { RobotSettingsDashboard } from '../pages/ODD/RobotSettingsDashboard' +import { ProtocolDashboard } from '../pages/ODD/ProtocolDashboard' +import { ProtocolDetails } from '../pages/ODD/ProtocolDetails' import { QuickTransferFlow } from '../organisms/QuickTransferFlow' -import { QuickTransferDashboard } from '../pages/QuickTransferDashboard' -import { QuickTransferDetails } from '../pages/QuickTransferDetails' -import { RunningProtocol } from '../pages/RunningProtocol' -import { RunSummary } from '../pages/RunSummary' -import { UpdateRobot } from '../pages/UpdateRobot/UpdateRobot' -import { UpdateRobotDuringOnboarding } from '../pages/UpdateRobot/UpdateRobotDuringOnboarding' -import { InstrumentsDashboard } from '../pages/InstrumentsDashboard' -import { InstrumentDetail } from '../pages/InstrumentDetail' -import { Welcome } from '../pages/Welcome' -import { InitialLoadingScreen } from '../pages/InitialLoadingScreen' -import { DeckConfigurationEditor } from '../pages/DeckConfiguration' +import { QuickTransferDashboard } from '../pages/ODD/QuickTransferDashboard' +import { QuickTransferDetails } from '../pages/ODD/QuickTransferDetails' +import { RunningProtocol } from '../pages/ODD/RunningProtocol' +import { RunSummary } from '../pages/ODD/RunSummary' +import { UpdateRobot } from '../pages/ODD/UpdateRobot/UpdateRobot' +import { UpdateRobotDuringOnboarding } from '../pages/ODD/UpdateRobot/UpdateRobotDuringOnboarding' +import { InstrumentsDashboard } from '../pages/ODD/InstrumentsDashboard' +import { InstrumentDetail } from '../pages/ODD/InstrumentDetail' +import { Welcome } from '../pages/ODD/Welcome' +import { InitialLoadingScreen } from '../pages/ODD/InitialLoadingScreen' +import { DeckConfigurationEditor } from '../pages/ODD/DeckConfiguration' import { PortalRoot as ModalPortalRoot } from './portal' import { getOnDeviceDisplaySettings, updateConfigValue } from '../redux/config' import { updateBrightness } from '../redux/shell' import { SLEEP_NEVER_MS } from './constants' -import { - useCurrentRunRoute, - useProtocolReceiptToast, - useSoftwareUpdatePoll, -} from './hooks' +import { useProtocolReceiptToast, useSoftwareUpdatePoll } from './hooks' +import { ODDTopLevelRedirects } from './ODDTopLevelRedirects' import { OnDeviceDisplayAppFallback } from './OnDeviceDisplayAppFallback' @@ -201,7 +198,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => { )} - + @@ -272,15 +269,6 @@ export function OnDeviceDisplayAppRoutes(): JSX.Element { ) } -function TopLevelRedirects(): JSX.Element | null { - const currentRunRoute = useCurrentRunRoute() - return currentRunRoute != null ? ( - - } /> - - ) : null -} - function ProtocolReceiptToasts(): null { useProtocolReceiptToast() return null diff --git a/app/src/App/__tests__/DesktopApp.test.tsx b/app/src/App/__tests__/DesktopApp.test.tsx index 8def97d4534..15a8ce5d136 100644 --- a/app/src/App/__tests__/DesktopApp.test.tsx +++ b/app/src/App/__tests__/DesktopApp.test.tsx @@ -7,31 +7,31 @@ import { vi, describe, beforeEach, afterEach, expect, it } from 'vitest' import { renderWithProviders } from '../../__testing-utils__' import { i18n } from '../../i18n' import { Breadcrumbs } from '../../organisms/Breadcrumbs' -import { CalibrationDashboard } from '../../pages/Devices/CalibrationDashboard' -import { DeviceDetails } from '../../pages/Devices/DeviceDetails' -import { DevicesLanding } from '../../pages/Devices/DevicesLanding' -import { ProtocolsLanding } from '../../pages/Protocols/ProtocolsLanding' -import { ProtocolRunDetails } from '../../pages/Devices/ProtocolRunDetails' -import { RobotSettings } from '../../pages/Devices/RobotSettings' -import { GeneralSettings } from '../../pages/AppSettings/GeneralSettings' +import { CalibrationDashboard } from '../../pages/Desktop/Devices/CalibrationDashboard' +import { DeviceDetails } from '../../pages/Desktop/Devices/DeviceDetails' +import { DevicesLanding } from '../../pages/Desktop/Devices/DevicesLanding' +import { ProtocolsLanding } from '../../pages/Desktop/Protocols/ProtocolsLanding' +import { ProtocolRunDetails } from '../../pages/Desktop/Devices/ProtocolRunDetails' +import { RobotSettings } from '../../pages/Desktop/Devices/RobotSettings' +import { GeneralSettings } from '../../pages/Desktop/AppSettings/GeneralSettings' import { AlertsModal } from '../../organisms/Alerts/AlertsModal' import { useFeatureFlag } from '../../redux/config' import { useIsFlex } from '../../organisms/Devices/hooks' -import { ProtocolTimeline } from '../../pages/Protocols/ProtocolDetails/ProtocolTimeline' +import { ProtocolTimeline } from '../../pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline' import { useSoftwareUpdatePoll } from '../hooks' import { DesktopApp } from '../DesktopApp' vi.mock('../../organisms/Breadcrumbs') vi.mock('../../organisms/Devices/hooks') -vi.mock('../../pages/AppSettings/GeneralSettings') -vi.mock('../../pages/Devices/CalibrationDashboard') -vi.mock('../../pages/Devices/DeviceDetails') -vi.mock('../../pages/Devices/DevicesLanding') -vi.mock('../../pages/Protocols/ProtocolsLanding') -vi.mock('../../pages/Devices/ProtocolRunDetails') -vi.mock('../../pages/Devices/RobotSettings') +vi.mock('../../pages/Desktop/AppSettings/GeneralSettings') +vi.mock('../../pages/Desktop/Devices/CalibrationDashboard') +vi.mock('../../pages/Desktop/Devices/DeviceDetails') +vi.mock('../../pages/Desktop/Devices/DevicesLanding') +vi.mock('../../pages/Desktop/Protocols/ProtocolsLanding') +vi.mock('../../pages/Desktop/Devices/ProtocolRunDetails') +vi.mock('../../pages/Desktop/Devices/RobotSettings') vi.mock('../../organisms/Alerts/AlertsModal') -vi.mock('../../pages/Protocols/ProtocolDetails/ProtocolTimeline') +vi.mock('../../pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline') vi.mock('../../redux/config') vi.mock('../hooks') diff --git a/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx b/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx index f088e2e6ac1..20816780b61 100644 --- a/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx +++ b/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx @@ -6,29 +6,30 @@ import { MemoryRouter } from 'react-router-dom' import { renderWithProviders } from '../../__testing-utils__' import { i18n } from '../../i18n' import { OnDeviceLocalizationProvider } from '../../LocalizationProvider' -import { ConnectViaEthernet } from '../../pages/ConnectViaEthernet' -import { ConnectViaUSB } from '../../pages/ConnectViaUSB' -import { ConnectViaWifi } from '../../pages/ConnectViaWifi' -import { NetworkSetupMenu } from '../../pages/NetworkSetupMenu' -import { InstrumentsDashboard } from '../../pages/InstrumentsDashboard' -import { RobotDashboard } from '../../pages/RobotDashboard' -import { RobotSettingsDashboard } from '../../pages/RobotSettingsDashboard' -import { ProtocolDashboard } from '../../pages/ProtocolDashboard' -import { ProtocolSetup } from '../../pages/ProtocolSetup' -import { ProtocolDetails } from '../../pages/ProtocolDetails' +import { ConnectViaEthernet } from '../../pages/ODD/ConnectViaEthernet' +import { ConnectViaUSB } from '../../pages/ODD/ConnectViaUSB' +import { ConnectViaWifi } from '../../pages/ODD/ConnectViaWifi' +import { NetworkSetupMenu } from '../../pages/ODD/NetworkSetupMenu' +import { InstrumentsDashboard } from '../../pages/ODD/InstrumentsDashboard' +import { RobotDashboard } from '../../pages/ODD/RobotDashboard' +import { RobotSettingsDashboard } from '../../pages/ODD/RobotSettingsDashboard' +import { ProtocolDashboard } from '../../pages/ODD/ProtocolDashboard' +import { ProtocolSetup } from '../../pages/ODD/ProtocolSetup' +import { ProtocolDetails } from '../../pages/ODD/ProtocolDetails' import { OnDeviceDisplayApp } from '../OnDeviceDisplayApp' -import { RunningProtocol } from '../../pages/RunningProtocol' -import { RunSummary } from '../../pages/RunSummary' -import { Welcome } from '../../pages/Welcome' -import { NameRobot } from '../../pages/NameRobot' -import { EmergencyStop } from '../../pages/EmergencyStop' -import { DeckConfigurationEditor } from '../../pages/DeckConfiguration' +import { RunningProtocol } from '../../pages/ODD/RunningProtocol' +import { RunSummary } from '../../pages/ODD/RunSummary' +import { Welcome } from '../../pages/ODD/Welcome' +import { NameRobot } from '../../pages/ODD/NameRobot' +import { EmergencyStop } from '../../pages/ODD/EmergencyStop' +import { DeckConfigurationEditor } from '../../pages/ODD/DeckConfiguration' import { getOnDeviceDisplaySettings } from '../../redux/config' import { getIsShellReady } from '../../redux/shell' import { getLocalRobot } from '../../redux/discovery' import { mockConnectedRobot } from '../../redux/discovery/__fixtures__' -import { useCurrentRunRoute, useProtocolReceiptToast } from '../hooks' +import { useProtocolReceiptToast } from '../hooks' import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' +import { ODDTopLevelRedirects } from '../ODDTopLevelRedirects' import type { UseQueryResult } from 'react-query' import type { RobotSettingsResponse } from '@opentrons/api-client' @@ -46,27 +47,28 @@ vi.mock('@opentrons/react-api-client', async () => { } }) vi.mock('../../LocalizationProvider') -vi.mock('../../pages/Welcome') -vi.mock('../../pages/NetworkSetupMenu') -vi.mock('../../pages/ConnectViaEthernet') -vi.mock('../../pages/ConnectViaUSB') -vi.mock('../../pages/ConnectViaWifi') -vi.mock('../../pages/RobotDashboard') -vi.mock('../../pages/RobotSettingsDashboard') -vi.mock('../../pages/ProtocolDashboard') -vi.mock('../../pages/ProtocolSetup') -vi.mock('../../pages/ProtocolDetails') -vi.mock('../../pages/InstrumentsDashboard') -vi.mock('../../pages/RunningProtocol') -vi.mock('../../pages/RunSummary') -vi.mock('../../pages/NameRobot') -vi.mock('../../pages/EmergencyStop') -vi.mock('../../pages/DeckConfiguration') +vi.mock('../../pages/ODD/Welcome') +vi.mock('../../pages/ODD/NetworkSetupMenu') +vi.mock('../../pages/ODD/ConnectViaEthernet') +vi.mock('../../pages/ODD/ConnectViaUSB') +vi.mock('../../pages/ODD/ConnectViaWifi') +vi.mock('../../pages/ODD/RobotDashboard') +vi.mock('../../pages/ODD/RobotSettingsDashboard') +vi.mock('../../pages/ODD/ProtocolDashboard') +vi.mock('../../pages/ODD/ProtocolSetup') +vi.mock('../../pages/ODD/ProtocolDetails') +vi.mock('../../pages/ODD/InstrumentsDashboard') +vi.mock('../../pages/ODD/RunningProtocol') +vi.mock('../../pages/ODD/RunSummary') +vi.mock('../../pages/ODD/NameRobot') +vi.mock('../../pages/ODD/EmergencyStop') +vi.mock('../../pages/ODD/DeckConfiguration') vi.mock('../../redux/config') vi.mock('../../redux/shell') vi.mock('../../redux/discovery') vi.mock('../../resources/maintenance_runs') vi.mock('../hooks') +vi.mock('../ODDTopLevelRedirects') const mockSettings = { sleepMs: 60 * 1000 * 60 * 24 * 7, @@ -88,7 +90,7 @@ describe('OnDeviceDisplayApp', () => { beforeEach(() => { vi.mocked(getOnDeviceDisplaySettings).mockReturnValue(mockSettings as any) vi.mocked(getIsShellReady).mockReturnValue(true) - vi.mocked(useCurrentRunRoute).mockReturnValue(null) + vi.mocked(ODDTopLevelRedirects).mockReturnValue(null) vi.mocked(getLocalRobot).mockReturnValue(mockConnectedRobot) vi.mocked(useNotifyCurrentMaintenanceRun).mockReturnValue({ data: { @@ -187,4 +189,9 @@ describe('OnDeviceDisplayApp', () => { render('/') expect(vi.mocked(useProtocolReceiptToast)).toHaveBeenCalled() }) + it('renders TopLevelRedirects when it should conditionally render', () => { + vi.mocked(ODDTopLevelRedirects).mockReturnValue(
MOCK_REDIRECTS
) + render('/') + screen.getByText('MOCK_REDIRECTS') + }) }) diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index 3d80704af1d..986d98698d3 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -10,24 +10,14 @@ import { useHost, useCreateLiveCommandMutation, } from '@opentrons/react-api-client' -import { - getProtocol, - RUN_ACTION_TYPE_PLAY, - RUN_STATUS_BLOCKED_BY_OPEN_DOOR, - RUN_STATUS_IDLE, - RUN_STATUS_STOPPED, - RUN_STATUS_FAILED, - RUN_STATUS_SUCCEEDED, -} from '@opentrons/api-client' +import { getProtocol } from '@opentrons/api-client' import { checkShellUpdate } from '../redux/shell' import { useToaster } from '../organisms/ToasterOven' -import { useCurrentRunId, useNotifyRunQuery } from '../resources/runs' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' import type { Dispatch } from '../redux/types' -const CURRENT_RUN_POLL = 5000 const UPDATE_RECHECK_INTERVAL_MS = 60000 const PROTOCOL_IDS_RECHECK_INTERVAL_MS = 3000 @@ -123,37 +113,3 @@ export function useProtocolReceiptToast(): void { // eslint-disable-next-line react-hooks/exhaustive-deps }, [protocolIds]) } - -export function useCurrentRunRoute(): string | null { - const currentRunId = useCurrentRunId({ refetchInterval: CURRENT_RUN_POLL }) - const { data: runRecord } = useNotifyRunQuery(currentRunId, { - staleTime: Infinity, - enabled: currentRunId != null, - }) - - const runStatus = runRecord?.data.status - const runActions = runRecord?.data.actions - if (runRecord == null || runStatus == null || runActions == null) return null - // grabbing run id off of the run query to have all routing info come from one source of truth - const runId = runRecord.data.id - const hasRunStarted = runActions?.some( - action => action.actionType === RUN_ACTION_TYPE_PLAY - ) - if ( - runStatus === RUN_STATUS_SUCCEEDED || - (runStatus === RUN_STATUS_STOPPED && hasRunStarted) || - runStatus === RUN_STATUS_FAILED - ) { - return `/runs/${runId}/summary` - } else if ( - runStatus === RUN_STATUS_IDLE || - (!hasRunStarted && runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR) - ) { - return `/runs/${runId}/setup` - } else if (hasRunStarted) { - return `/runs/${runId}/run` - } else { - // includes runs cancelled before starting and runs not yet started - return null - } -} diff --git a/app/src/LocalizationProvider.tsx b/app/src/LocalizationProvider.tsx index e2a30c95cd7..7fbb30d4774 100644 --- a/app/src/LocalizationProvider.tsx +++ b/app/src/LocalizationProvider.tsx @@ -10,8 +10,8 @@ export interface OnDeviceLocalizationProviderProps { children?: React.ReactNode } -const BRANDED_RESOURCE = 'branded' -const ANONYMOUS_RESOURCE = 'anonymous' +export const BRANDED_RESOURCE = 'branded' +export const ANONYMOUS_RESOURCE = 'anonymous' // TODO(bh, 2024-03-26): anonymization limited to ODD for now, may change in future OEM phases export function OnDeviceLocalizationProvider( diff --git a/app/src/assets/localization/__tests__/branded.test.ts b/app/src/assets/localization/__tests__/branded.test.ts new file mode 100644 index 00000000000..ed53aabbaec --- /dev/null +++ b/app/src/assets/localization/__tests__/branded.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest' +import { + ANONYMOUS_RESOURCE, + BRANDED_RESOURCE, +} from '../../../LocalizationProvider' +import { resources } from '..' + +describe('branded copy', () => { + it('branded and anonymous resources contain the same translation keys', () => { + const brandedKeys = Object.keys(resources.en[BRANDED_RESOURCE]) + const anonymousKeys = Object.keys(resources.en[ANONYMOUS_RESOURCE]) + + brandedKeys.forEach((brandedKey, i) => { + const anonymousKey = anonymousKeys[i] + expect(brandedKey).toEqual(anonymousKey) + }) + }) + + it('non-branded copy does not contain "Opentrons" or "Flex"', () => { + const nonBrandedResources = Object.entries(resources.en).filter( + resource => + resource[0] !== BRANDED_RESOURCE && resource[0] !== ANONYMOUS_RESOURCE + ) + + const nonBrandedCopy = nonBrandedResources + .map(resource => Object.values(resource[1])) + .flat() + + nonBrandedCopy.forEach(phrase => { + expect(phrase.match(/opentrons/i)).toBeNull() + expect(phrase.match(/flex/i)).toBeNull() + }) + }) +}) diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json index 12e57d595fa..221dfd72400 100644 --- a/app/src/assets/localization/en/anonymous.json +++ b/app/src/assets/localization/en/anonymous.json @@ -2,8 +2,8 @@ "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the desktop app. Go to Robot", "about_flex_gripper": "About Gripper", "alternative_security_types_description": "The robot supports connecting to various enterprise access points. Connect via USB and finish setup in the desktop app.", - "attach_a_pipette": "Attach a pipette to your robot", "attach_a_pipette_for_quick_transfer": "To create a quick transfer, you need to attach a pipette to your robot.", + "attach_a_pipette": "Attach a pipette to your robot", "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", "choose_what_data_to_share": "Choose what robot data to share.", @@ -37,17 +37,19 @@ "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your pipette.", "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact support.", "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your robot.", + "new_robot_instructions": "When setting up a new robot, follow the instructions on the touchscreen. For more information, consult the Quickstart Guide for your robot.", "oem_mode_description": "Enable OEM Mode to remove all instances of Opentrons from the Flex touchscreen.", "opentrons_app_successfully_updated": "The app was successfully updated.", - "opentrons_app_update": "app update", - "opentrons_app_update_available": "App Update Available", "opentrons_app_update_available_variation": "An app update is available.", + "opentrons_app_update_available": "App Update Available", + "opentrons_app_update": "app update", "opentrons_app_will_use_interpreter": "If specified, the app will use the Python interpreter at this path instead of the default bundled Python interpreter.", "opentrons_cares_about_privacy": "We care about your privacy. We anonymize all data and only use it to improve our products.", "opentrons_def": "Verified Definition", + "opentrons_flex_quickstart_guide": "Quickstart Guide", "opentrons_labware_def": "Verified labware definition", - "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", "opentrons_tip_rack_name": "opentrons", + "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", "previous_releases": "View previous releases", "receive_alert": "Receive an alert when a software update is available.", "restore_description": "Reverting to previous software versions is not recommended, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", @@ -58,11 +60,11 @@ "secure_labware_explanation_thermocycler": "Secure your labware to the Thermocycler Module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", "send_a_protocol_to_store": "Send a protocol to the robot to get started.", "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box.", - "share_app_analytics": "Share App Analytics", "share_app_analytics_description": "Help improve this product by automatically sending anonymous diagnostics and usage data.", + "share_app_analytics": "Share App Analytics", "share_display_usage_description": "Data on how you interact with the robot's touchscreen.", - "share_logs_with_opentrons": "Share robot logs", "share_logs_with_opentrons_description": "Help improve this product by automatically sending anonymous robot logs. These logs are used to troubleshoot robot issues and spot error trends.", + "share_logs_with_opentrons": "Share robot logs", "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the app. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact support for assistance.", "storage_limit_reached_description": "Your robot has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json index 6a65184183f..2b5f47373e8 100644 --- a/app/src/assets/localization/en/branded.json +++ b/app/src/assets/localization/en/branded.json @@ -1,9 +1,9 @@ { "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", - "attach_a_pipette": "Attach a pipette to your Flex", - "attach_a_pipette_for_quick_transfer": "To create a quick transfer, you need to attach a pipette to your Opentrons Flex.", "about_flex_gripper": "About Flex Gripper", "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", + "attach_a_pipette_for_quick_transfer": "To create a quick transfer, you need to attach a pipette to your Opentrons Flex.", + "attach_a_pipette": "Attach a pipette to your Flex", "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", "choose_what_data_to_share": "Choose what data to share with Opentrons.", @@ -37,14 +37,16 @@ "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your Opentrons Flex.", + "new_robot_instructions": "When setting up a new Flex, follow the instructions on the touchscreen. For more information, consult the Quickstart Guide for your robot.", "oem_mode_description": "Enable OEM Mode to remove all instances of Opentrons from the Flex touchscreen.", "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", - "opentrons_app_update": "Opentrons App update", - "opentrons_app_update_available": "Opentrons App Update Available", "opentrons_app_update_available_variation": "An Opentrons App update is available.", + "opentrons_app_update_available": "Opentrons App Update Available", + "opentrons_app_update": "Opentrons App update", "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", "opentrons_cares_about_privacy": "Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.", "opentrons_def": "Opentrons Definition", + "opentrons_flex_quickstart_guide": "Opentrons Flex Quickstart Guide", "opentrons_labware_def": "Opentrons labware definition", "opentrons_tip_rack_name": "opentrons", "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", @@ -58,11 +60,11 @@ "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler Module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", "send_a_protocol_to_store": "Send a protocol from the Opentrons App to get started.", "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box or scan the QR code to visit the modules section of the Opentrons Help Center.", - "share_app_analytics": "Share App Analytics with Opentrons", "share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", + "share_app_analytics": "Share App Analytics with Opentrons", "share_display_usage_description": "Data on how you interact with the touchscreen on Flex.", - "share_logs_with_opentrons": "Share Robot logs with Opentrons", "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", + "share_logs_with_opentrons": "Share Robot logs with Opentrons", "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact Opentrons Support for assistance.", "storage_limit_reached_description": "Your Opentrons Flex has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index 70a31da2a0d..6ee3d06b399 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -139,6 +139,7 @@ "recalibrate_pipette": "Recalibrate pipette", "recent_protocol_runs": "Recent Protocol Runs", "rerun_now": "Rerun protocol now", + "rerun_loading": "Protocol re-run is disabled until data connection fully loads", "reset_all": "Reset all", "reset_estop": "Reset E-stop", "resume_operation": "Resume operation", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index e7e21e013b0..5e40c7ce5e2 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -24,6 +24,7 @@ "calibration": "Calibration", "calibration_health_check_description": "Check the accuracy of key calibration points without recalibrating the robot.", "calibration_health_check_title": "Calibration Health Check", + "cancel_software_update": "Cancel software update", "change_network": "Change network", "characters_max": "17 characters max", "check_for_updates": "Check for updates", @@ -50,7 +51,6 @@ "clear_option_runs_history": "Clear protocol run history", "clear_option_runs_history_subtext": "Clears information about past runs of all protocols.", "clear_option_tip_length_calibrations": "Clear tip length calibrations", - "cancel_software_update": "Cancel software update", "complete_and_restart_robot": "Complete and restart robot", "confirm_device_reset_description": "This will permanently delete all protocol, calibration, and other data. You’ll have to redo initial setup before using the robot again.", "confirm_device_reset_heading": "Are you sure you want to reset your device?", @@ -81,7 +81,6 @@ "device_reset_description": "Reset labware calibration, boot scripts, and/or robot calibration to factory settings.", "device_reset_slideout_description": "Select individual settings to only clear specific data types.", "device_resets_cannot_be_undone": "Resets cannot be undone", - "release_notes": "Release notes", "directly_connected_to_this_computer": "Directly connected to this computer.", "disconnect": "Disconnect", "disconnect_from_ssid": "Disconnect from {{ssid}}", @@ -194,8 +193,8 @@ "not_now": "Not now", "oem_mode": "OEM Mode", "off": "Off", - "one_hour": "1 hour", "on": "On", + "one_hour": "1 hour", "other_networks": "Other Networks", "password": "Password", "password_error_message": "Must be at least 8 characters", @@ -220,6 +219,7 @@ "recalibrate_tip_and_pipette": "Recalibrate Tip Length and Pipette Offset", "recalibration_recommended": "Recalibration recommended", "reinstall": "reinstall", + "release_notes": "Release notes", "remind_me_later": "Remind me later", "rename_robot": "Rename robot", "rename_robot_input_error": "Oops! Robot name must follow the character count and limitations.", @@ -290,13 +290,13 @@ "update_channel_description": "Stable receives the latest stable releases. Beta allows you to try out new in-progress features before they launch in Stable channel, but they have not completed testing yet.", "update_complete": "Update complete!", "update_found": "Update found!", + "update_requires_restarting_robot": "Updating the robot software requires restarting the robot", "update_robot_now": "Update robot now", "update_robot_software": "Update robot software manually with a local file (.zip)", "updating": "Updating", - "update_requires_restarting_robot": "Updating the robot software requires restarting the robot", + "upload_custom_logo": "Upload custom logo", "upload_custom_logo_description": "Upload a logo for the robot to display during boot up.", "upload_custom_logo_dimensions": "The logo must fit within dimensions 1024 x 600 and be a PNG file (.png).", - "upload_custom_logo": "Upload custom logo", "usage_settings": "Usage Settings", "usb": "USB", "usb_to_ethernet_description": "Looking for USB-to-Ethernet Adapter info?", @@ -319,6 +319,7 @@ "wpa2_personal": "WPA2 Personal", "wpa2_personal_description": "Most labs use this method", "yes_clear_data_and_restart_robot": "Yes, clear data and restart robot", + "you_should_not_downgrade": "You should not downgrade to a software version released before the manufacture date of your robot or any attached hardware.", "your_mac_address_is": "Your MAC Address is {{macAddress}}", "your_robot_is_ready_to_go": "Your robot is ready to go." } diff --git a/app/src/assets/localization/en/devices_landing.json b/app/src/assets/localization/en/devices_landing.json index dfd92d23030..1ff5ef11fd6 100644 --- a/app/src/assets/localization/en/devices_landing.json +++ b/app/src/assets/localization/en/devices_landing.json @@ -25,11 +25,9 @@ "looking_for_robots": "Looking for robots", "make_sure_robot_is_connected": "Make sure the robot is connected to this computer", "modules": "Modules", - "new_robot_instructions": "When setting up a new Flex, follow the instructions on the touchscreen. For more information, consult the Quickstart Guide for your robot.", "ninety_six_mount": "Left + Right Mount", "no_robots_found": "No robots found", "not_available": "Not available ({{count}})", - "opentrons_flex_quickstart_guide": "Opentrons Flex Quickstart Guide", "ot2_quickstart_guide": "OT-2 Quickstart Guide", "refresh": "Refresh", "restart_the_app": "Restart the app", diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 484ec67124a..379a047cda8 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -57,6 +57,7 @@ "pause_on": "Pause on {{robot_name}}", "pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}", "prepare_to_aspirate": "Preparing {{pipette}} to aspirate", + "reloading_labware": "Reloading {{labware}}", "return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}", "right": "Right", "save_position": "Saving position", @@ -65,6 +66,8 @@ "setting_temperature_module_temp": "Setting Temperature Module to {{temp}} (rounded to nearest integer)", "setting_thermocycler_block_temp": "Setting Thermocycler block temperature to {{temp}} with hold time of {{hold_time_seconds}} seconds after target reached", "setting_thermocycler_lid_temp": "Setting Thermocycler lid temperature to {{temp}}", + "turning_rail_lights_off": "Turning rail lights off", + "turning_rail_lights_on": "Turning rail lights on", "slot": "Slot {{slot_name}}", "target_temperature": "target temperature", "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", diff --git a/app/src/assets/localization/en/protocol_details.json b/app/src/assets/localization/en/protocol_details.json index 19345f0b8a5..b93e675a1c5 100644 --- a/app/src/assets/localization/en/protocol_details.json +++ b/app/src/assets/localization/en/protocol_details.json @@ -42,6 +42,7 @@ "name": "Name", "no_available_robots_found": "No available robots found", "no_custom_values": "No custom values specified", + "no_labware_specified": "No labware specified in this protocol", "no_parameters": "No parameters specified in this protocol", "no_summary": "no summary specified for this protocol.", "not_connected": "not connected", diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index 78bd118c1ab..3307c45363f 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -25,6 +25,7 @@ "import_a_file": "Import a protocol to get started", "import_new_protocol": "Import a Protocol", "import": "Import", + "incompatible_file_type": "Incompatible file type", "instrument_cal_data_title": "Calibration data", "instrument_not_attached": "Not attached", "instruments_title": "Required Pipettes", diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index ee00797352e..f2e284e607e 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -1,26 +1,29 @@ { "96_mount": "left + right mount", "action_needed": "Action needed", - "adapter_slot_location_module": "Slot {{slotName}}, {{adapterName}} on {{moduleName}}", "adapter_slot_location": "Slot {{slotName}}, {{adapterName}}", + "adapter_slot_location_module": "Slot {{slotName}}, {{adapterName}} on {{moduleName}}", "add_fixture": "Add {{fixtureName}} to {{locationName}}", "add_this_deck_hardware": "Add this hardware to your deck configuration. It will be referenced during protocol analysis.", "add_to_slot": "Add to slot {{slotName}}", "additional_labware": "{{count}} additional labware", "additional_off_deck_labware": "Additional Off-Deck Labware", + "all_files_associated": "All files associated with the protocol run are available on the robot detail screen.", + "applied_labware_offset_data": "Applied labware offset data", "applied_labware_offsets": "applied labware offsets", "are_you_sure_you_want_to_proceed": "Are you sure you want to proceed to run?", - "attach_gripper_failure_reason": "Attach the required gripper to continue", + "attach": "attach", "attach_gripper": "attach gripper", + "attach_gripper_failure_reason": "Attach the required gripper to continue", "attach_module": "Attach module before calibrating", "attach_pipette_before_module_calibration": "Attach a pipette before running module calibration", "attach_pipette_calibration": "Attach pipette to see calibration information", "attach_pipette_cta": "Attach Pipette", "attach_pipette_failure_reason": "Attach the required pipette(s) to continue", "attach_pipette_tip_length_calibration": "Attach pipette to see tip length calibration information", - "attach": "attach", "back_to_top": "Back to top", "cal_all_pip": "Calibrate pipettes first", + "calibrate": "calibrate", "calibrate_deck_failure_reason": "Calibrate the deck to continue", "calibrate_deck_to_proceed_to_pipette_calibration": "Calibrate your deck in order to proceed to pipette calibration", "calibrate_deck_to_proceed_to_tip_length_calibration": "Calibrate your deck in order to proceed to tip length calibration", @@ -30,16 +33,15 @@ "calibrate_pipette_before_module_calibration": "Calibrate pipette before running module calibration", "calibrate_pipette_failure_reason": "Calibrate the required pipette(s) to continue", "calibrate_tiprack_failure_reason": "Calibrate the required tip lengths to continue", - "calibrate": "calibrate", "calibrated": "calibrated", + "calibration": "Calibration", "calibration_data_not_available": "Calibration data not available once run has started", "calibration_needed": "Calibration needed", "calibration_ready": "Calibration ready", + "calibration_required": "Calibration required", "calibration_required_attach_pipette_first": "Calibration required Attach pipette first", "calibration_required_calibrate_pipette_first": "Calibration required Calibrate pipette first", - "calibration_required": "Calibration required", "calibration_status": "calibration status", - "calibration": "Calibration", "cancel_and_restart_to_edit": "Cancel the run and restart setup to edit", "choose_csv_file": "Choose CSV file", "choose_enum": "Choose {{displayName}}", @@ -49,9 +51,9 @@ "configured": "configured", "confirm_heater_shaker_module_modal_description": "Before the run begins, module should have both anchors fully extended for a firm attachment. The thermal adapter should be attached to the module. ", "confirm_heater_shaker_module_modal_title": "Confirm Heater-Shaker Module is attached", - "confirm_offsets": "Confirm offsets", "confirm_liquids": "Confirm liquids", "confirm_locations_and_volumes": "Confirm locations and volumes", + "confirm_offsets": "Confirm offsets", "confirm_placements": "Confirm placements", "confirm_selection": "Confirm selection", "confirm_values": "Confirm values", @@ -66,79 +68,80 @@ "currently_configured": "Currently configured", "currently_unavailable": "Currently unavailable", "custom_values": "Custom values", + "deck_cal_description": "This measures the deck X and Y values relative to the gantry. Deck Calibration is the foundation for Tip Length Calibration and Pipette Offset Calibration.", "deck_cal_description_bullet_1": "Perform Deck Calibration during new robot setup.", "deck_cal_description_bullet_2": "Redo Deck Calibration if you relocate your robot.", - "deck_cal_description": "This measures the deck X and Y values relative to the gantry. Deck Calibration is the foundation for Tip Length Calibration and Pipette Offset Calibration.", "deck_calibration_title": "Deck Calibration", - "deck_conflict_info_thermocycler": "Update the deck configuration by removing the fixtures in locations A1 and B1. Either remove the fixtures from the deck configuration or update the protocol.", - "deck_conflict_info": "Update the deck configuration by removing the {{currentFixture}} in location {{cutout}}. Either remove the fixture from the deck configuration or update the protocol.", "deck_conflict": "Deck location conflict", + "deck_conflict_info": "Update the deck configuration by removing the {{currentFixture}} in location {{cutout}}. Either remove the fixture from the deck configuration or update the protocol.", + "deck_conflict_info_thermocycler": "Update the deck configuration by removing the fixtures in locations A1 and B1. Either remove the fixtures from the deck configuration or update the protocol.", "deck_hardware": "Deck hardware", "deck_hardware_ready": "Deck hardware ready", "deck_map": "Deck Map", "default_values": "Default values", + "download_files": "Download files", "example": "Example", "exit_to_deck_configuration": "Exit to deck configuration", "extension_mount": "extension mount", "extra_attention_warning_title": "Secure labware and modules before proceeding to run", "extra_module_attached": "Extra module attached", "feedback_form_link": "Let us know!", - "fixture_name": "fixture", "fixture": "Fixture", - "fixtures_connected_plural": "{{count}} fixtures attached", + "fixture_name": "fixture", "fixtures_connected": "{{count}} fixture attached", + "fixtures_connected_plural": "{{count}} fixtures attached", "get_labware_offset_data": "Get Labware Offset Data", "hardware_missing": "Missing hardware", "heater_shaker_extra_attention": "Use latch controls for easy placement of labware.", "heater_shaker_labware_list_view": "To add labware, use the toggle to control the latch", "how_offset_data_works": "How labware offsets work", "individiual_well_volume": "Individual well volume", - "initial_liquids_num_plural": "{{count}} initial liquids", "initial_liquids_num": "{{count}} initial liquid", + "initial_liquids_num_plural": "{{count}} initial liquids", "initial_location": "Initial Location", + "install_modules": "Install the required module.", "install_modules_and_fixtures": "Install and calibrate the required modules. Install the required fixtures.", "install_modules_plural": "Install the required modules.", - "install_modules": "Install the required module.", - "instrument_calibrations_missing_plural": "Missing {{count}} calibrations", "instrument_calibrations_missing": "Missing {{count}} calibration", - "instruments_connected_plural": "{{count}} instruments attached", - "instruments_connected": "{{count}} instrument attached", + "instrument_calibrations_missing_plural": "Missing {{count}} calibrations", "instruments": "Instruments", - "labware_latch_instructions": "Use latch control for easy placement of labware.", + "instruments_connected": "{{count}} instrument attached", + "instruments_connected_plural": "{{count}} instruments attached", + "labware": "Labware", "labware_latch": "Labware Latch", + "labware_latch_instructions": "Use latch control for easy placement of labware.", "labware_location": "Labware Location", "labware_name": "Labware name", "labware_placement": "labware placement", + "labware_position_check": "Labware Position Check", + "labware_position_check_not_available": "Labware Position Check is not available after run has started", "labware_position_check_not_available_analyzing_on_robot": "Labware Position Check is not available while protocol is analyzing on robot", "labware_position_check_not_available_empty_protocol": "Labware Position Check requires that the protocol loads labware and pipettes", - "labware_position_check_not_available": "Labware Position Check is not available after run has started", "labware_position_check_step_description": "Recommended workflow that helps you verify the position of each labware on the deck.", "labware_position_check_step_title": "Labware Position Check", "labware_position_check_text": "Labware Position Check is a recommended workflow that helps you verify the position of each labware on the deck. During this check, you can create Labware Offsets that adjust how the robot moves to each labware in the X, Y and Z directions.", - "labware_position_check": "Labware Position Check", "labware_setup_step_description": "Gather the following labware and full tip racks. To run your protocol without Labware Position Check, place and secure labware in their initial locations.", "labware_setup_step_title": "Labware", - "labware": "Labware", "last_calibrated": "Last calibrated: {{date}}", "learn_how_it_works": "Learn how it works", + "learn_more": "Learn more", "learn_more_about_offset_data": "Learn more about Labware Offset Data", "learn_more_about_robot_cal_link": "Learn more about robot calibration", - "learn_more": "Learn more", "liquid_information": "Liquid information", "liquid_name": "Liquid name", - "liquids": "liquids", "liquid_setup_step_description": "View liquid starting locations and volumes", "liquid_setup_step_title": "Liquids", + "liquids": "liquids", + "liquids_confirmed": "Liquids confirmed", "liquids_not_in_setup": "No liquids used in this protocol", "liquids_not_in_the_protocol": "no liquids are specified for this protocol.", "liquids_ready": "Liquids ready", - "liquids_confirmed": "Liquids confirmed", "list_view": "List View", "loading_data": "Loading data...", "loading_labware_offsets": "Loading labware offsets", "loading_protocol_details": "Loading details...", - "location_conflict": "Location conflict", "location": "Location", + "location_conflict": "Location conflict", "lpc_and_offset_data_title": "Labware Position Check and Labware Offset Data", "lpc_disabled_calibration_not_complete": "Make sure robot calibration is complete before running Labware Position Check", "lpc_disabled_modules_and_calibration_not_complete": "Make sure robot calibration is complete and all modules are connected before running Labware Position Check", @@ -146,36 +149,36 @@ "lpc_disabled_no_tipracks_loaded": "Labware Position Check requires that the protocol loads a tip rack", "lpc_disabled_no_tipracks_used": "Labware Position Check requires that the protocol has at least one pipette that picks up a tip", "map_view": "Map View", + "missing": "Missing", "missing_gripper": "Missing gripper", "missing_instruments": "Missing {{count}}", - "missing_pipettes_plural": "Missing {{count}} pipettes", "missing_pipettes": "Missing {{count}} pipette", - "missing": "Missing", + "missing_pipettes_plural": "Missing {{count}} pipettes", "modal_instructions_title": "{{moduleName}} Setup Instructions", + "module": "Module", "module_connected": "Connected", "module_disconnected": "Disconnected", "module_instructions_link": "{{moduleName}} setup instructions", "module_mismatch_body": "Check that the modules connected to this robot are of the right type and generation", "module_name": "Module", "module_not_connected": "Not connected", - "module_setup_step_title": "Deck hardware", "module_setup_step_ready": "Calibration ready", + "module_setup_step_title": "Deck hardware", "module_slot_location": "Slot {{slotName}}, {{moduleName}}", - "module": "Module", - "modules_connected_plural": "{{count}} modules attached", + "modules": "Modules", "modules_connected": "{{count}} module attached", + "modules_connected_plural": "{{count}} modules attached", "modules_setup_step_title": "Module Setup", - "modules": "Modules", - "mount_title": "{{mount}} MOUNT:", "mount": "{{mount}} mount", + "mount_title": "{{mount}} MOUNT:", "multiple_fixtures_missing": "{{count}} fixtures missing", + "multiple_modules": "Multiple modules of the same type", "multiple_modules_example": "Your protocol has two Temperature Modules. The Temperature Module attached to the first port starting from the left will be related to the first Temperature Module in your protocol while the second Temperature Module loaded would be related to the Temperature Module connected to the next port to the right. If using a hub, follow the same logic with the port ordering.", "multiple_modules_explanation": "To use more than one of the same module in a protocol, you first need to plug in the module that’s called first in your protocol to the lowest numbered USB port on the robot. Continue in the same manner with additional modules.", "multiple_modules_help_link_title": "See How To Set Up Modules of the Same Type", "multiple_modules_learn_more": "Learn more about using multiple modules of the same type", "multiple_modules_missing_plural": "Missing {{count}} modules", "multiple_modules_modal": "Setting up multiple modules of the same type", - "multiple_modules": "Multiple modules of the same type", "multiple_of_most_modules": "You can use multiples of most module types within a single Python protocol by connecting and loading the modules in a specific order. The robot will initialize the matching module attached to the lowest numbered port first, regardless of what deck slot it occupies.", "must_have_labware_and_pip": "Protocol must load labware and a pipette", "n_a": "N/A", @@ -188,8 +191,8 @@ "no_modules_or_fixtures": "No modules or fixtures are specified for this protocol.", "no_modules_specified": "no modules are specified for this protocol.", "no_modules_used_in_this_protocol": "No hardware used in this protocol", - "no_parameters_specified_in_protocol": "No parameters specified in this protocol", "no_parameters_specified": "No parameters specified", + "no_parameters_specified_in_protocol": "No parameters specified in this protocol", "no_tiprack_loaded": "Protocol must load a tip rack", "no_tiprack_used": "Protocol must pick up a tip", "no_usb_connection_required": "No USB connection required", @@ -197,32 +200,32 @@ "no_usb_required": "No USB required", "not_calibrated": "Not calibrated yet", "not_configured": "not configured", - "off_deck": "Off deck", "off": "Off", + "off_deck": "Off deck", "offset_data": "Offset Data", - "offsets_applied_plural": "{{count}} offsets applied", - "offsets_applied": "{{count}} offset applied", + "offsets_applied": "{{count}} offsets applied", + "offsets_confirmed": "Offsets confirmed", "offsets_ready": "Offsets ready", - "on_adapter_in_mod": "on {{adapterName}} in {{moduleName}}", + "on": "On", + "on-deck_labware": "{{count}} on-deck labware", "on_adapter": "on {{adapterName}}", + "on_adapter_in_mod": "on {{adapterName}} in {{moduleName}}", "on_deck": "On deck", - "on-deck_labware": "{{count}} on-deck labware", - "on": "On", "opening": "Opening...", "parameters": "Parameters", "pipette_mismatch": "Pipette generation mismatch.", "pipette_missing": "Pipette missing", + "pipette_offset_cal": "Pipette Offset Calibration", + "pipette_offset_cal_description": "This measures a pipette’s X, Y and Z values in relation to the pipette mount and the deck. Pipette Offset Calibration relies on Deck Calibration and Tip Length Calibration. ", "pipette_offset_cal_description_bullet_1": "Perform Pipette Offset calibration the first time you attach a pipette to a new mount.", "pipette_offset_cal_description_bullet_2": "Redo Pipette Offset Calibration after performing Deck Calibration.", "pipette_offset_cal_description_bullet_3": "Redo Pipette Offset Calibration after performing Tip Length Calibration for the tip you used to calibrate the pipette.", - "pipette_offset_cal_description": "This measures a pipette’s X, Y and Z values in relation to the pipette mount and the deck. Pipette Offset Calibration relies on Deck Calibration and Tip Length Calibration. ", - "pipette_offset_cal": "Pipette Offset Calibration", "placement": "Placement", - "placements_ready": "Placements ready", "placements_confirmed": "Placements confirmed", + "placements_ready": "Placements ready", "plug_in_module_to_configure": "Plug in a {{module}} to add it to the slot", - "plug_in_required_module_plural": "Plug in and power up the required modules to continue", "plug_in_required_module": "Plug in and power up the required module to continue", + "plug_in_required_module_plural": "Plug in and power up the required modules to continue", "prepare_to_run": "Prepare to run", "proceed_to_labware_position_check": "Proceed to labware position check", "proceed_to_labware_setup_step": "Proceed to labware", @@ -244,34 +247,34 @@ "recalibrating_not_available": "Recalibrating Tip Length calibrations and Labware Position Check is not available.", "recalibrating_tip_length_not_available": "Recalibrating a tip length is not available once a run has started", "recommended": "Recommended", + "required": "Required", "required_instrument_calibrations": "required instrument calibrations", "required_tip_racks_title": "Required Tip Length Calibrations", - "required": "Required", - "reset_parameter_values_body": "This will discard any changes you have made. All parameters will have their default values.", "reset_parameter_values": "Reset parameter values?", + "reset_parameter_values_body": "This will discard any changes you have made. All parameters will have their default values.", "reset_setup": "Restart setup to edit", "reset_values": "Reset values", "resolve": "Resolve", - "restart_setup_and_try": "Restart setup and try using different parameter values.", "restart_setup": "Restart setup", + "restart_setup_and_try": "Restart setup and try using different parameter values.", "restore_default": "Restore default value", "restore_defaults": "Restore default values", "robot_cal_description": "Robot calibration establishes how the robot knows where it is in relation to the deck. Accurate Robot calibration is essential to run protocols successfully. Robot calibration has 3 parts: Deck calibration, Tip Length calibration and Pipette Offset calibration.", "robot_cal_help_title": "How Robot Calibration Works", "robot_calibration_step_description_pipettes_only": "Review required instruments and calibrations for this protocol.", "robot_calibration_step_description": "Review required pipettes and tip length calibrations for this protocol.", - "robot_calibration_step_title": "Instruments", "robot_calibration_step_ready": "Calibration ready", + "robot_calibration_step_title": "Instruments", + "run": "Run", "run_disabled_calibration_not_complete": "Make sure robot calibration is complete before proceeding to run", "run_disabled_modules_and_calibration_not_complete": "Make sure robot calibration is complete and all modules are connected before proceeding to run", "run_disabled_modules_not_connected": "Make sure all modules are connected before proceeding to run", "run_labware_position_check": "run labware position check", "run_labware_position_check_to_get_offsets": "Run Labware Position Check to get your labware offset data.", "run_never_started": "Run was never started", - "run": "Run", + "secure": "Secure", "secure_labware_instructions": "Secure labware instructions", "secure_labware_modal": "Securing labware to the {{name}}", - "secure": "Secure", "setup_for_run": "Setup for Run", "setup_instructions": "setup instructions", "setup_is_view_only": "Setup is view-only once run has started", @@ -283,22 +286,22 @@ "step": "STEP {{index}}", "there_are_no_unconfigured_modules": "No {{module}} is connected. Attach one and place it in {{slot}}.", "there_are_other_configured_modules": "A {{module}} is already configured in a different slot. Exit run setup and update your deck configuration to move to an already connected module. Or attach another {{module}} to continue setup.", - "tip_length_cal_description_bullet": "Perform Tip Length Calibration for each new tip type used on a pipette.", "tip_length_cal_description": "This measures the Z distance between the bottom of the tip and the pipette’s nozzle. If you redo the tip length calibration for the tip you used to calibrate a pipette, you will also have to redo that Pipette Offset Calibration.", + "tip_length_cal_description_bullet": "Perform Tip Length Calibration for each new tip type used on a pipette.", "tip_length_cal_title": "Tip Length Calibration", "tip_length_calibration": "tip length calibration", "total_liquid_volume": "Total volume", - "update_deck_config": "Update deck configuration", "update_deck": "Update deck", + "update_deck_config": "Update deck configuration", "update_offsets": "Update offsets", "updated": "Updated", "usb_connected_no_port_info": "USB Port Connected", "usb_drive_notification": "Leave USB drive attached until run starts", "usb_port_connected": "USB Port {{port}}", "usb_port_number": "USB-{{port}}", - "value_out_of_range_generic": "Value must be in range", - "value_out_of_range": "Value must be between {{min}}-{{max}}", "value": "Value", + "value_out_of_range": "Value must be between {{min}}-{{max}}", + "value_out_of_range_generic": "Value must be in range", "values_are_view_only": "Values are view-only", "variable_well_amount": "Variable well amount", "view_current_offsets": "View current offsets", diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index b754376c81a..0cb31912964 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -7,6 +7,7 @@ "air_gap": "Air gap", "air_gap_before_aspirating": "Air gap before aspirating", "air_gap_before_dispensing": "Air gap before dispensing", + "air_gap_capacity_error": "The tip is too full to add an air gap.", "air_gap_value": "{{volume}} µL", "air_gap_volume_µL": "Air gap volume (µL)", "all": "All labware", @@ -33,6 +34,8 @@ "character_limit_error": "Character limit exceeded", "column": "column", "columns": "columns", + "consolidate_volume_error": "The selected destination well is too small to consolidate into. Try consolidating from fewer wells.", + "create_new_to_edit": "Create a new quick transfer to edit", "create_new_transfer": "Create new quick transfer", "create_transfer": "Create transfer", "delay": "Delay", @@ -56,6 +59,7 @@ "dispense_volume_µL": "Dispense volume per well (µL)", "disposal_volume_µL": "Disposal volume (µL)", "distance_bottom_of_well_mm": "Distance from bottom of well (mm)", + "distribute_volume_error": "The selected source well is too small to distribute from. Try distributing to fewer wells.", "enter_characters": "Enter up to 60 characters", "error_analyzing": "An error occurred while attempting to analyze {{transferName}}.", "exit_quick_transfer": "Exit quick transfer?", @@ -92,7 +96,7 @@ "pipette_path": "Pipette path", "pipette_path_multi_aspirate": "Multi-aspirate", "pipette_path_multi_dispense": "Multi-dispense", - "pipette_path_multi_dispense_volume_blowout": "Multi-dispense, {{volume}} disposal volume, blowout into {{blowOutLocation}}", + "pipette_path_multi_dispense_volume_blowout": "Multi-dispense, {{volume}} disposal volume, blowout {{blowOutLocation}}", "pipette_path_single": "Single transfers", "pre_wet_tip": "Pre-wet tip", "quick_transfer": "Quick transfer", @@ -117,7 +121,7 @@ "set_transfer_volume": "Set transfer volume", "source": "Source", "source_labware": "Source labware", - "source_labware_d2": "Source labware in D2", + "source_labware_c2": "Source labware in C2", "starting_well": "starting well", "storage_limit_reached": "Storage limit reached", "tip_drop_location": "Tip drop location", @@ -151,5 +155,5 @@ "well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well).", "well_selection": "Well selection", "wells": "wells", - "will_be_deleted": " will be permanently deleted." + "will_be_deleted": "{{transferName}} will be permanently deleted." } diff --git a/app/src/assets/localization/en/robot_calibration.json b/app/src/assets/localization/en/robot_calibration.json index 1e96d8d1c24..723e1526adc 100644 --- a/app/src/assets/localization/en/robot_calibration.json +++ b/app/src/assets/localization/en/robot_calibration.json @@ -91,7 +91,7 @@ "pipette_offset_recalibrate_both_mounts": "Pipette offsets for both mounts will have to be recalibrated.", "pipette_offset_requires_tip_length": "You don’t have a tip length saved with this pipette yet. You will need to calibrate tip length before calibrating your pipette offset.", "pipette_offset_title": "pipette offset calibration", - "place_cal_block": "Place the Calibration Block into it's designated slot", + "place_cal_block": "Place the Calibration Block into its designated slot", "place_full_tip_rack": "Place a full {{tip_rack}} into slot 8", "position_pipette_over_tip": "Position pipette over A1", "prepare_the_space": "Prepare the space", diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index ef56e913613..f8ce9a7590e 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -105,7 +105,6 @@ "run_complete": "Run completed", "run_completed": "Run completed.", "run_completed_splash": "Run completed", - "run_completed_with_errors": "Run completed with errors.", "run_completed_with_warnings": "Run completed with warnings.", "run_completed_with_warnings_splash": "Run completed with warnings", "run_cta_disabled": "Complete required steps on Protocol tab before starting the run", diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx index 8c41120d536..7bb9a73e105 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -13,6 +13,7 @@ interface NumericalKeyboardProps { isDecimal?: boolean hasHyphen?: boolean debug?: boolean + initialValue?: string } // the default keyboard layout intKeyboard that doesn't have decimal point and hyphen. @@ -22,6 +23,7 @@ export function NumericalKeyboard({ isDecimal = false, hasHyphen = false, debug = false, + initialValue = '', }: NumericalKeyboardProps): JSX.Element { const layoutName = `${isDecimal ? 'float' : 'int'}${ hasHyphen ? 'NegKeyboard' : 'Keyboard' @@ -35,6 +37,9 @@ export function NumericalKeyboard({ (keyboardRef.current = r)} theme={'hg-theme-default oddTheme1 numerical-keyboard'} + onInit={keyboard => { + keyboard.setInput(initialValue) + }} onChange={onChange} display={numericalCustom} useButtonTag={true} diff --git a/app/src/molecules/Command/__fixtures__/mockRobotSideAnalysis.json b/app/src/molecules/Command/__fixtures__/mockRobotSideAnalysis.json index b4a041b36f9..cd2bd35c802 100644 --- a/app/src/molecules/Command/__fixtures__/mockRobotSideAnalysis.json +++ b/app/src/molecules/Command/__fixtures__/mockRobotSideAnalysis.json @@ -6431,6 +6431,20 @@ }, "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" } + }, + { + "id": "84f7af1d-c097-4d4b-9819-ad56479bbbb8", + "createdAt": "2023-01-31T21:53:04.965216+00:00", + "commandType": "reloadLabware", + "key": "1248111104", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5" + }, + "result": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "offsetId": "offsetId-abc123" + } } ], "errors": [], diff --git a/app/src/molecules/Command/__tests__/CommandText.test.tsx b/app/src/molecules/Command/__tests__/CommandText.test.tsx index 9226f258830..09856d77ba1 100644 --- a/app/src/molecules/Command/__tests__/CommandText.test.tsx +++ b/app/src/molecules/Command/__tests__/CommandText.test.tsx @@ -588,6 +588,23 @@ describe('CommandText', () => { ) screen.getByText('Load NEST 96 Well Plate 100 µL PCR Full Skirt off deck') }) + it('renders correct text for reloadLabware', () => { + const reloadLabwareCommand = mockCommandTextData.commands.find( + c => c.commandType === 'reloadLabware' + ) + expect(reloadLabwareCommand).not.toBeUndefined() + if (reloadLabwareCommand != null) { + renderWithProviders( + , + { i18nInstance: i18n } + ) + } + screen.getByText('Reloading NEST 96 Well Plate 100 µL PCR Full Skirt (1)') + }) it('renders correct text for loadLiquid', () => { const loadLabwareCommands = mockCommandTextData.commands.filter( c => c.commandType === 'loadLabware' diff --git a/app/src/molecules/Command/hooks/useCommandTextString/index.tsx b/app/src/molecules/Command/hooks/useCommandTextString/index.tsx index 093593dc911..d10a9aa3211 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/index.tsx +++ b/app/src/molecules/Command/hooks/useCommandTextString/index.tsx @@ -71,6 +71,7 @@ export function useCommandTextString( } case 'loadLabware': + case 'reloadLabware': case 'loadPipette': case 'loadModule': case 'loadLiquid': @@ -216,6 +217,11 @@ export function useCommandTextString( commandText: utils.getCustomCommandText({ ...fullParams, command }), } + case 'setRailLights': + return { + commandText: utils.getRailLightsCommandText({ ...fullParams, command }), + } + case undefined: case null: return { commandText: '' } diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getLoadCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getLoadCommandText.ts index b2da948d58d..e28d52f3959 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/utils/getLoadCommandText.ts +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getLoadCommandText.ts @@ -140,6 +140,14 @@ export const getLoadCommandText = ({ }) } } + case 'reloadLabware': { + const { labwareId } = command.params + const labware = + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null + return t('reloading_labware', { labware }) + } case 'loadLiquid': { const { liquidId, labwareId } = command.params return t('load_liquids_info_protocol_setup', { diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getRailLightsCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getRailLightsCommandText.ts new file mode 100644 index 00000000000..b731d3ec392 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getRailLightsCommandText.ts @@ -0,0 +1,15 @@ +import type { RunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +type HandledCommands = Extract + +export type GetRailLightsCommandText = HandlesCommands + +export function getRailLightsCommandText({ + command, + t, +}: GetRailLightsCommandText): string { + return command.params.on + ? t('turning_rail_lights_on') + : t('turning_rail_lights_off') +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts index f7946ff1e47..ff3ad43fc8c 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts @@ -21,3 +21,4 @@ export { getCustomCommandText } from './getCustomCommandText' export { getUnknownCommandText } from './getUnknownCommandText' export { getPipettingCommandText } from './getPipettingCommandText' export { getLiquidProbeCommandText } from './getLiquidProbeCommandText' +export { getRailLightsCommandText } from './getRailLightsCommandText' diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx index 62a6cccfe0e..9802f6d28b5 100644 --- a/app/src/molecules/UploadInput/index.tsx +++ b/app/src/molecules/UploadInput/index.tsx @@ -29,8 +29,7 @@ const StyledLabel = styled.label` text-align: ${TYPOGRAPHY.textAlignCenter}; background-color: ${COLORS.white}; - &:hover, - &:focus-within { + &:hover { border: 2px dashed ${COLORS.blue50}; } ` diff --git a/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx b/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx index f3882525ef8..aea0cf5591f 100644 --- a/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx +++ b/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx @@ -11,7 +11,7 @@ import { renderWithProviders } from '../../../__testing-utils__' import { AddCustomLabwareSlideout } from '..' vi.mock('../../../redux/custom-labware') -vi.mock('../../../pages/Labware/helpers/getAllDefs') +vi.mock('../../../pages/Desktop/Labware/helpers/getAllDefs') vi.mock('../../../redux/analytics') let mockTrackEvent: any diff --git a/app/src/organisms/CalibrationPanels/__tests__/DeckSetup.test.tsx b/app/src/organisms/CalibrationPanels/__tests__/DeckSetup.test.tsx index d52e42581b1..0533e492232 100644 --- a/app/src/organisms/CalibrationPanels/__tests__/DeckSetup.test.tsx +++ b/app/src/organisms/CalibrationPanels/__tests__/DeckSetup.test.tsx @@ -74,7 +74,7 @@ describe('DeckSetup', () => { screen.getByRole('heading', { name: 'Prepare the space' }) screen.getByText('Place a full 300ul Tiprack FIXTURE into slot 8') - screen.getByText("Place the Calibration Block into it's designated slot") + screen.getByText('Place the Calibration Block into its designated slot') expect(screen.queryByText('To check the left pipette:')).toBeNull() expect(screen.queryByText('Clear all other deck slots')).toBeNull() }) @@ -88,9 +88,7 @@ describe('DeckSetup', () => { screen.getByRole('heading', { name: 'Prepare the space' }) screen.getByText('Place a full 300ul Tiprack FIXTURE into slot 8') expect( - screen.queryByText( - "Place the Calibration Block into it's designated slot" - ) + screen.queryByText('Place the Calibration Block into its designated slot') ).toBeNull() expect(screen.queryByText('To check the left pipette:')).toBeNull() expect(screen.queryByText('Clear all other deck slots')).toBeNull() @@ -105,7 +103,7 @@ describe('DeckSetup', () => { screen.getByRole('heading', { name: 'Prepare the space' }) screen.getByText('Place a full fake tiprack display name into slot 8') - screen.getByText("Place the Calibration Block into it's designated slot") + screen.getByText('Place the Calibration Block into its designated slot') screen.getByText('To check the left pipette:') screen.getByText('Clear all other deck slots') }) @@ -122,9 +120,7 @@ describe('DeckSetup', () => { screen.getByText('To check the left pipette:') screen.getByText('Clear all other deck slots') expect( - screen.queryByText( - "Place the Calibration Block into it's designated slot" - ) + screen.queryByText('Place the Calibration Block into its designated slot') ).toBeNull() }) }) diff --git a/app/src/organisms/CalibrationTaskList/index.tsx b/app/src/organisms/CalibrationTaskList/index.tsx index 77f0590c304..92db27bd75a 100644 --- a/app/src/organisms/CalibrationTaskList/index.tsx +++ b/app/src/organisms/CalibrationTaskList/index.tsx @@ -27,9 +27,9 @@ import { } from '../Devices/hooks' import { useCurrentRunId } from '../../resources/runs' -import type { DashboardCalOffsetInvoker } from '../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset' -import type { DashboardCalTipLengthInvoker } from '../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' -import type { DashboardCalDeckInvoker } from '../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck' +import type { DashboardCalOffsetInvoker } from '../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset' +import type { DashboardCalTipLengthInvoker } from '../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' +import type { DashboardCalDeckInvoker } from '../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck' interface CalibrationTaskListProps { robotName: string @@ -120,6 +120,7 @@ export function CalibrationTaskList({ width: 50rem; height: 47.5rem; `} + marginLeft="0" > {showCompletionScreen ? ( ) : ( - + { setCurrentPage(1) }} - width="51%" + width="50%" > {t('shared:change_protocol')} {isCreatingRun ? ( - + + + {t('shared:confirm_values')} + ) : ( t('shared:confirm_values') )} diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 057533ce778..f201f47f539 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -16,7 +16,7 @@ import { getUnreachableRobots, startDiscovery, } from '../../../redux/discovery' -import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../../redux/robot-update' import { mockConnectableRobot, mockReachableRobot, @@ -72,11 +72,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { mockTrackCreateProtocolRunEvent = vi.fn( () => new Promise(resolve => resolve({})) ) - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: '', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(false) vi.mocked(getConnectableRobots).mockReturnValue([mockConnectableRobot]) vi.mocked(getUnreachableRobots).mockReturnValue([mockUnreachableRobot]) vi.mocked(getReachableRobots).mockReturnValue([mockReachableRobot]) @@ -221,11 +217,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() }) it('if selected robot is on a different version of the software than the app, disable CTA and show link to device details in options', () => { - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(true) render({ storedProtocolData: storedProtocolDataFixture, onCloseClick: vi.fn(), @@ -320,7 +312,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { runCreatedAt: '2022-05-11T13:33:51.012179+00:00', } when(vi.mocked(useOffsetCandidatesForAnalysis)) - .calledWith(storedProtocolDataFixture.mostRecentAnalysis, '127.0.0.1') + .calledWith(storedProtocolDataFixture.mostRecentAnalysis, null) .thenReturn([mockOffsetCandidate]) vi.mocked(getConnectableRobots).mockReturnValue([ mockConnectableRobot, @@ -333,7 +325,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { }) expect(vi.mocked(useCreateRunFromProtocol)).toHaveBeenCalledWith( expect.any(Object), - { hostname: '127.0.0.1' }, + null, [ { vector: mockOffsetCandidate.vector, @@ -369,11 +361,8 @@ describe('ChooseRobotToRunProtocolSlideout', () => { runCreatedAt: '2022-05-11T13:33:51.012179+00:00', } when(vi.mocked(useOffsetCandidatesForAnalysis)) - .calledWith(storedProtocolDataFixture.mostRecentAnalysis, '127.0.0.1') + .calledWith(storedProtocolDataFixture.mostRecentAnalysis, null) .thenReturn([mockOffsetCandidate]) - when(vi.mocked(useOffsetCandidatesForAnalysis)) - .calledWith(storedProtocolDataFixture.mostRecentAnalysis, 'otherIp') - .thenReturn([]) vi.mocked(getConnectableRobots).mockReturnValue([ mockConnectableRobot, { ...mockConnectableRobot, name: 'otherRobot', ip: 'otherIp' }, @@ -393,10 +382,9 @@ describe('ChooseRobotToRunProtocolSlideout', () => { }) fireEvent.click(proceedButton) fireEvent.click(screen.getByRole('button', { name: 'Confirm values' })) - expect(vi.mocked(useCreateRunFromProtocol)).nthCalledWith( - 3, + expect(vi.mocked(useCreateRunFromProtocol)).toHaveBeenLastCalledWith( expect.any(Object), - { hostname: '127.0.0.1' }, + null, [ { vector: mockOffsetCandidate.vector, @@ -405,11 +393,6 @@ describe('ChooseRobotToRunProtocolSlideout', () => { }, ] ) - expect(vi.mocked(useCreateRunFromProtocol)).toHaveBeenLastCalledWith( - expect.any(Object), - { hostname: 'otherIp' }, - [] - ) }) it('disables proceed button if no available robots', () => { diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 67c5b2c2e5a..5e9a4fe7241 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -1,23 +1,27 @@ import * as React from 'react' import first from 'lodash/first' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' import { + ALIGN_CENTER, DIRECTION_COLUMN, DIRECTION_ROW, Flex, Icon, + NO_WRAP, PrimaryButton, SecondaryButton, SPACING, Tooltip, useHoverTooltip, } from '@opentrons/components' -import { useUploadCsvFileMutation } from '@opentrons/react-api-client' +import { + useUploadCsvFileMutation, + ApiHostProvider, +} from '@opentrons/react-api-client' -import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../redux/robot-update' import { OPENTRONS_USB } from '../../redux/discovery' import { appShellRequestor } from '../../redux/shell/remote' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' @@ -31,7 +35,6 @@ import { ChooseRobotSlideout } from '../ChooseRobotSlideout' import { useCreateRunFromProtocol } from './useCreateRunFromProtocol' import type { StyleProps } from '@opentrons/components' import type { RunTimeParameter } from '@opentrons/shared-data' -import type { State } from '../../redux/types' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' @@ -44,11 +47,23 @@ interface ChooseRobotToRunProtocolSlideoutProps extends StyleProps { showSlideout: boolean } +interface ChooseRobotToRunProtocolSlideoutComponentProps + extends ChooseRobotToRunProtocolSlideoutProps { + selectedRobot: Robot | null + setSelectedRobot: (robot: Robot | null) => void +} + export function ChooseRobotToRunProtocolSlideoutComponent( - props: ChooseRobotToRunProtocolSlideoutProps + props: ChooseRobotToRunProtocolSlideoutComponentProps ): JSX.Element | null { const { t } = useTranslation(['protocol_details', 'shared', 'app_settings']) - const { storedProtocolData, showSlideout, onCloseClick } = props + const { + storedProtocolData, + showSlideout, + onCloseClick, + selectedRobot, + setSelectedRobot, + } = props const navigate = useNavigate() const [shouldApplyOffsets, setShouldApplyOffsets] = React.useState( true @@ -60,7 +75,6 @@ export function ChooseRobotToRunProtocolSlideoutComponent( mostRecentAnalysis, } = storedProtocolData const [currentPage, setCurrentPage] = React.useState(1) - const [selectedRobot, setSelectedRobot] = React.useState(null) const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( storedProtocolData, selectedRobot?.name ?? '' @@ -81,19 +95,10 @@ export function ChooseRobotToRunProtocolSlideoutComponent( const offsetCandidates = useOffsetCandidatesForAnalysis( mostRecentAnalysis, - selectedRobot?.ip ?? null + null ) - const { uploadCsvFile } = useUploadCsvFileMutation( - {}, - selectedRobot != null - ? { - hostname: selectedRobot.ip, - requestor: - selectedRobot?.ip === OPENTRONS_USB ? appShellRequestor : undefined, - } - : null - ) + const { uploadCsvFile } = useUploadCsvFileMutation() const { createRunFromProtocolSource, @@ -119,13 +124,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( }) }, }, - selectedRobot != null - ? { - hostname: selectedRobot.ip, - requestor: - selectedRobot?.ip === OPENTRONS_USB ? appShellRequestor : undefined, - } - : null, + null, shouldApplyOffsets ? offsetCandidates.map(({ vector, location, definitionUri }) => ({ vector, @@ -145,7 +144,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( : acc, {} ) - Promise.all( + void Promise.all( Object.entries(dataFilesForProtocolMap).map(([key, file]) => { const fileResponse = uploadCsvFile(file) const varName = Promise.resolve(key) @@ -173,15 +172,10 @@ export function ChooseRobotToRunProtocolSlideoutComponent( }) } - const { autoUpdateAction } = useSelector((state: State) => - getRobotUpdateDisplayInfo(state, selectedRobot?.name ?? '') + const isSelectedRobotOnDifferentSoftwareVersion = useIsRobotOnWrongVersionOfSoftware( + selectedRobot?.name ?? '' ) - const isSelectedRobotOnDifferentSoftwareVersion = [ - 'upgrade', - 'downgrade', - ].includes(autoUpdateAction) - const hasRunTimeParameters = runTimeParameters.length > 0 if ( @@ -202,9 +196,9 @@ export function ChooseRobotToRunProtocolSlideoutComponent( first(srcFileNames) ?? protocolKey - // intentionally show both robot types if analysis has any error + // intentionally show both robot types if analysis fails const robotType = - mostRecentAnalysis != null && mostRecentAnalysis.errors.length === 0 + mostRecentAnalysis != null && mostRecentAnalysis.result !== 'not-ok' ? mostRecentAnalysis?.robotType ?? null : null @@ -258,7 +252,11 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) : ( - + { setCurrentPage(1) @@ -274,7 +272,15 @@ export function ChooseRobotToRunProtocolSlideoutComponent( {...targetProps} > {isCreatingRun ? ( - + + + {t('shared:confirm_values')} + ) : ( t('shared:confirm_values') )} @@ -347,5 +353,18 @@ export function ChooseRobotToRunProtocolSlideoutComponent( export function ChooseRobotToRunProtocolSlideout( props: ChooseRobotToRunProtocolSlideoutProps ): JSX.Element | null { - return + const [selectedRobot, setSelectedRobot] = React.useState(null) + return ( + + + + ) } diff --git a/app/src/organisms/Devices/HistoricalProtocolRunDrawer.tsx b/app/src/organisms/Devices/HistoricalProtocolRunDrawer.tsx index 8ccecf1746c..6108bdd5ee5 100644 --- a/app/src/organisms/Devices/HistoricalProtocolRunDrawer.tsx +++ b/app/src/organisms/Devices/HistoricalProtocolRunDrawer.tsx @@ -104,7 +104,7 @@ export function HistoricalProtocolRunDrawer( const protocolFilesData = runDataFileIds.length === 0 ? ( - + ) : ( {t('protocol_files')} @@ -151,13 +151,8 @@ export function HistoricalProtocolRunDrawer( const labwareOffsets = uniqueLabwareOffsets == null || uniqueLabwareOffsets.length === 0 ? ( - + ) : ( - // {outOfDateBanner} diff --git a/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx b/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx index 2e1a661ccb5..4432fd8bd3e 100644 --- a/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx +++ b/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx @@ -1,11 +1,11 @@ import * as React from 'react' -import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { NavLink, useNavigate } from 'react-router-dom' import { ALIGN_CENTER, ALIGN_FLEX_END, + FLEX_MAX_CONTENT, Box, COLORS, DIRECTION_COLUMN, @@ -31,12 +31,11 @@ import { ANALYTICS_PROTOCOL_PROCEED_TO_RUN, ANALYTICS_PROTOCOL_RUN_ACTION, } from '../../redux/analytics' -import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../redux/robot-update' import { useDownloadRunLog, useTrackProtocolRunEvent, useRobot } from './hooks' import { useIsEstopNotDisengaged } from '../../resources/devices/hooks/useIsEstopNotDisengaged' import type { Run } from '@opentrons/api-client' -import type { State } from '../../redux/types' export interface HistoricalProtocolRunOverflowMenuProps { runId: string @@ -114,11 +113,10 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { isRunLogLoading, } = props - const isRobotOnWrongVersionOfSoftware = ['upgrade', 'downgrade'].includes( - useSelector((state: State) => { - return getRobotUpdateDisplayInfo(state, robotName) - })?.autoUpdateAction + const isRobotOnWrongVersionOfSoftware = useIsRobotOnWrongVersionOfSoftware( + robotName ) + const [targetProps, tooltipProps] = useHoverTooltip() const onResetSuccess = (createRunResponse: Run): void => { navigate( @@ -133,7 +131,7 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { } const trackEvent = useTrackEvent() const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) - const { reset } = useRunControls(runId, onResetSuccess) + const { reset, isRunControlLoading } = useRunControls(runId, onResetSuccess) const { deleteRun } = useDeleteRunMutation() const robot = useRobot(robotName) const robotSerialNumber = @@ -165,7 +163,6 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { return ( @@ -183,7 +181,9 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { {t('rerun_now')} @@ -193,6 +193,11 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { {t('shared:a_software_update_is_available')} )} + {isRunControlLoading && ( + + {t('rerun_loading')} + + )} | null - robotName: string - runId: string - makeHandleJumpToStep: (index: number) => () => void - missingSetupSteps: string[] -} - -export function ProtocolRunHeader({ - protocolRunHeaderRef, - robotName, - runId, - makeHandleJumpToStep, - missingSetupSteps, -}: ProtocolRunHeaderProps): JSX.Element | null { - const { t } = useTranslation(['run_details', 'shared']) - const navigate = useNavigate() - const host = useHost() - const createdAtTimestamp = useRunCreatedAtTimestamp(runId) - const { - protocolData, - displayName, - protocolKey, - isProtocolAnalyzing, - } = useProtocolDetailsForRun(runId) - const { reportRecoveredRunResult } = useRecoveryAnalytics() - - const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) - const robotAnalyticsData = useRobotAnalyticsData(robotName) - const isRobotViewable = useIsRobotViewable(robotName) - const runStatus = useRunStatus(runId) - const { analysisErrors } = useProtocolAnalysisErrors(runId) - const isRunCurrent = Boolean( - useNotifyRunQuery(runId, { refetchInterval: CURRENT_RUN_POLL_MS })?.data - ?.data?.current - ) - const mostRecentRunId = useMostRecentRunId() - const isMostRecentRun = mostRecentRunId === runId - const { closeCurrentRun, isClosingCurrentRun } = useCloseCurrentRun() - const { startedAt, stoppedAt, completedAt } = useRunTimestamps(runId) - const [showRunFailedModal, setShowRunFailedModal] = React.useState(false) - const { data: commandErrorList } = useRunCommandErrors( - runId, - { cursor: 0, pageLength: 100 }, - { - enabled: - runStatus != null && - // @ts-expect-error runStatus expected to possibly not be terminal - RUN_STATUSES_TERMINAL.includes(runStatus) && - isMostRecentRun, - } - ) - const isResetRunLoadingRef = React.useRef(false) - const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) - const highestPriorityError = - runRecord?.data.errors?.[0] != null - ? getHighestPriorityError(runRecord?.data?.errors) - : null - - const robotSettings = useSelector((state: State) => - getRobotSettings(state, robotName) - ) - const isFlex = useIsFlex(robotName) - const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) - const robotType = isFlex ? FLEX_ROBOT_TYPE : OT2_ROBOT_TYPE - const deckConfigCompatibility = useDeckConfigurationCompatibility( - robotType, - robotProtocolAnalysis - ) - const isFixtureMismatch = getIsFixtureMismatch(deckConfigCompatibility) - const { isERActive, failedCommand } = useErrorRecoveryFlows(runId, runStatus) - - const doorSafetySetting = robotSettings.find( - setting => setting.id === 'enableDoorSafetySwitch' - ) - const { data: doorStatus } = useDoorQuery({ - refetchInterval: EQUIPMENT_POLL_MS, - }) - let isDoorOpen: boolean - if (isFlex) { - isDoorOpen = doorStatus?.data.status === 'open' - } else if (!isFlex && Boolean(doorSafetySetting?.value)) { - isDoorOpen = doorStatus?.data.status === 'open' - } else { - isDoorOpen = false - } - - const { showDTWiz, toggleDTWiz } = useDropTipWizardFlows() - const { - areTipsAttached, - determineTipStatus, - resetTipStatus, - setTipStatusResolved, - aPipetteWithTip, - initialPipettesWithTipsCount, - } = useTipAttachmentStatus({ - runId, - runRecord: runRecord ?? null, - host, - }) - const { - showDTModal, - onDTModalSkip, - onDTModalRemoval, - isDisabled: areDTModalBtnsDisabled, - } = useProtocolDropTipModal({ - areTipsAttached, - toggleDTWiz, - isRunCurrent, - currentRunId: runId, - instrumentModelSpecs: aPipetteWithTip?.specs, - mount: aPipetteWithTip?.mount, - robotType, - onSkipAndHome: () => { - closeCurrentRun() - }, - }) - - const enteredER = runRecord?.data.hasEverEnteredErrorRecovery ?? false - const cancelledWithoutRecovery = - !enteredER && runStatus === RUN_STATUS_STOPPED - - React.useEffect(() => { - if (isFlex) { - if (runStatus === RUN_STATUS_IDLE) { - resetTipStatus() - } else if ( - runStatus != null && - // @ts-expect-error runStatus expected to possibly not be terminal - RUN_STATUSES_TERMINAL.includes(runStatus) && - enteredER === false - ) { - void determineTipStatus() - } - } - }, [runStatus]) - - React.useEffect(() => { - if (protocolData != null && !isRobotViewable) { - navigate('/devices') - } - }, [protocolData, isRobotViewable, navigate]) - - React.useEffect(() => { - if (isRunCurrent && typeof enteredER === 'boolean') { - reportRecoveredRunResult(runStatus, enteredER) - } - }, [isRunCurrent, enteredER]) - - // Side effects dependent on the current run state. - React.useEffect(() => { - if (runStatus === RUN_STATUS_STOPPED && isRunCurrent && runId != null) { - trackProtocolRunEvent({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.FINISH, - properties: { - ...robotAnalyticsData, - }, - }) - - // TODO(jh, 08-15-24): The enteredER condition is a hack, because errorCommands are only returned when a run is current. - // Ideally the run should not need to be current to view errorCommands. - - // Close the run if no tips are attached after running tip check at least once. - // This marks the robot as "not busy" as soon as a run is cancelled if drop tip CTAs are unnecessary. - if (initialPipettesWithTipsCount === 0 && !enteredER) { - closeCurrentRun() - } - } - }, [runStatus, isRunCurrent, runId, enteredER]) - - const startedAtTimestamp = - startedAt != null ? formatTimestamp(startedAt) : EMPTY_TIMESTAMP - - const completedAtTimestamp = - completedAt != null ? formatTimestamp(completedAt) : EMPTY_TIMESTAMP - - // redirect to new run after successful reset - const onResetSuccess = (createRunResponse: Run): void => { - navigate( - `/devices/${robotName}/protocol-runs/${createRunResponse.data.id}/run-preview` - ) - } - - const { pause, play } = useRunControls(runId, onResetSuccess) - - const [showAnalysisErrorModal, setShowAnalysisErrorModal] = React.useState( - false - ) - const handleErrorModalCloseClick: React.MouseEventHandler = e => { - e.preventDefault() - e.stopPropagation() - setShowAnalysisErrorModal(false) - } - React.useEffect(() => { - if (analysisErrors != null && analysisErrors?.length > 0) { - setShowAnalysisErrorModal(true) - } - }, [analysisErrors]) - - const [ - showConfirmCancelModal, - setShowConfirmCancelModal, - ] = React.useState(false) - - const handleCancelClick = (): void => { - if (runStatus === RUN_STATUS_RUNNING) pause() - setShowConfirmCancelModal(true) - } - - const handleClearClick = (): void => { - trackProtocolRunEvent({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.FINISH, - properties: robotAnalyticsData ?? undefined, - }) - closeCurrentRun() - } - - return ( - <> - {isERActive ? ( - - ) : null} - {showRunFailedModal ? ( - - ) : null} - - {showAnalysisErrorModal && - analysisErrors != null && - analysisErrors.length > 0 && ( - - )} - - {protocolKey != null ? ( - - - {displayName} - - - ) : ( - - {displayName} - - )} - - {analysisErrors != null && analysisErrors.length > 0 && ( - - )} - {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( - - {t('close_door_to_resume')} - - ) : null} - {runStatus === RUN_STATUS_STOPPED && !enteredER ? ( - - {t('run_canceled')} - - ) : null} - {/* Note: This banner is for before running a protocol */} - {isDoorOpen && - runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR && - runStatus !== RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR && - runStatus != null && - CANCELLABLE_STATUSES.includes(runStatus) ? ( - - {t('shared:close_robot_door')} - - ) : null} - {isMostRecentRun ? ( - - ) : null} - {showDTModal ? ( - - ) : null} - - - } - /> - - } - /> - - - - - {runStatus != null ? ( - - - - - {CANCELLABLE_STATUSES.includes(runStatus) && ( - - {t('cancel_run')} - - )} - - - ) : null} - - {showConfirmCancelModal ? ( - { - setShowConfirmCancelModal(false) - }} - runId={runId} - robotName={robotName} - /> - ) : null} - {showDTWiz && aPipetteWithTip != null ? ( - { - if (isTakeover) { - toggleDTWiz() - } else { - void setTipStatusResolved(() => { - toggleDTWiz() - closeCurrentRun() - }, toggleDTWiz) - } - }} - /> - ) : null} - - - ) -} - -interface LabeledValueProps { - label: string - value: React.ReactNode -} - -function LabeledValue(props: LabeledValueProps): JSX.Element { - return ( - - - {props.label} - - {typeof props.value === 'string' ? ( - {props.value} - ) : ( - props.value - )} - - ) -} - -interface DisplayRunStatusProps { - runStatus: RunStatus | null -} - -function DisplayRunStatus(props: DisplayRunStatusProps): JSX.Element { - const { t } = useTranslation('run_details') - return ( - - {props.runStatus === RUN_STATUS_RUNNING ? ( - - - - ) : null} - - {props.runStatus != null ? t(`status_${String(props.runStatus)}`) : ''} - - - ) -} - -const START_RUN_STATUSES: RunStatus[] = [ - RUN_STATUS_IDLE, - RUN_STATUS_PAUSED, - RUN_STATUS_BLOCKED_BY_OPEN_DOOR, -] -const RUN_AGAIN_STATUSES: RunStatus[] = [ - RUN_STATUS_STOPPED, - RUN_STATUS_FINISHING, - RUN_STATUS_FAILED, - RUN_STATUS_SUCCEEDED, -] -const RECOVERY_STATUSES: RunStatus[] = [ - RUN_STATUS_AWAITING_RECOVERY, - RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, - RUN_STATUS_AWAITING_RECOVERY_PAUSED, -] -const DISABLED_STATUSES: RunStatus[] = [ - RUN_STATUS_FINISHING, - RUN_STATUS_STOP_REQUESTED, - RUN_STATUS_BLOCKED_BY_OPEN_DOOR, - ...RECOVERY_STATUSES, -] -interface ActionButtonProps { - runId: string - robotName: string - runStatus: RunStatus | null - isProtocolAnalyzing: boolean - isDoorOpen: boolean - isFixtureMismatch: boolean - isResetRunLoadingRef: React.MutableRefObject - missingSetupSteps: string[] -} - -// TODO(jh, 04-22-2024): Refactor switch cases into separate factories to increase readability and testability. -function ActionButton(props: ActionButtonProps): JSX.Element { - const { - runId, - robotName, - runStatus, - isProtocolAnalyzing, - isDoorOpen, - isFixtureMismatch, - isResetRunLoadingRef, - missingSetupSteps, - } = props - const navigate = useNavigate() - const { t } = useTranslation(['run_details', 'shared']) - const attachedModules = - useModulesQuery({ - refetchInterval: EQUIPMENT_POLL_MS, - enabled: runStatus != null && START_RUN_STATUSES.includes(runStatus), - })?.data?.data ?? [] - const trackEvent = useTrackEvent() - const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) - const [targetProps, tooltipProps] = useHoverTooltip() - const { - play, - pause, - reset, - isPlayRunActionLoading, - isPauseRunActionLoading, - isResetRunLoading, - } = useRunControls(runId, (createRunResponse: Run): void => - // redirect to new run after successful reset - { - navigate( - `/devices/${robotName}/protocol-runs/${createRunResponse.data.id}/run-preview` - ) - } - ) - isResetRunLoadingRef.current = isResetRunLoading - const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId) - const { complete: isCalibrationComplete } = useRunCalibrationStatus( - robotName, - runId - ) - const { complete: isModuleCalibrationComplete } = useModuleCalibrationStatus( - robotName, - runId - ) - const [showIsShakingModal, setShowIsShakingModal] = React.useState(false) - const isSetupComplete = - isCalibrationComplete && - isModuleCalibrationComplete && - missingModuleIds.length === 0 - const isRobotOnWrongVersionOfSoftware = ['upgrade', 'downgrade'].includes( - useSelector((state: State) => { - return getRobotUpdateDisplayInfo(state, robotName) - })?.autoUpdateAction - ) - const currentRunId = useCurrentRunId() - const isCurrentRun = currentRunId === runId - const isOtherRunCurrent = currentRunId != null && currentRunId !== runId - const isRunControlButtonDisabled = - (isCurrentRun && !isSetupComplete) || - isPlayRunActionLoading || - isPauseRunActionLoading || - isResetRunLoading || - isOtherRunCurrent || - isProtocolAnalyzing || - isFixtureMismatch || - (runStatus != null && DISABLED_STATUSES.includes(runStatus)) || - isRobotOnWrongVersionOfSoftware || - // For before running a protocol, "close door to begin". - (isDoorOpen && - runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR && - runStatus != null && - CANCELLABLE_STATUSES.includes(runStatus)) - const robot = useRobot(robotName) - const robotSerialNumber = - robot?.status != null ? getRobotSerialNumber(robot) : null ?? '' - const handleProceedToRunClick = (): void => { - trackEvent({ - name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, - properties: { robotSerialNumber }, - }) - play() - } - const configBypassHeaterShakerAttachmentConfirmation = useSelector( - getIsHeaterShakerAttached - ) - const { - confirm: confirmAttachment, - showConfirmation: showHSConfirmationModal, - cancel: cancelExitHSConfirmation, - } = useConditionalConfirm( - handleProceedToRunClick, - !configBypassHeaterShakerAttachmentConfirmation - ) - const { - confirm: confirmMissingSteps, - showConfirmation: showMissingStepsConfirmationModal, - cancel: cancelExitMissingStepsConfirmation, - } = useConditionalConfirm( - handleProceedToRunClick, - missingSetupSteps.length !== 0 - ) - const robotAnalyticsData = useRobotAnalyticsData(robotName) - - const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() - const activeHeaterShaker = attachedModules.find( - (module): module is HeaterShakerModule => - module.moduleType === 'heaterShakerModuleType' && - module?.data != null && - module.data.speedStatus !== 'idle' - ) - const isHeaterShakerShaking = attachedModules - .filter((module): module is HeaterShakerModule => { - return module.moduleType === 'heaterShakerModuleType' - }) - .some(module => module?.data != null && module.data.speedStatus !== 'idle') - const isValidRunAgain = - runStatus != null && RUN_AGAIN_STATUSES.includes(runStatus) - const validRunAgainButRequiresSetup = isValidRunAgain && !isSetupComplete - const runAgainWithSpinner = validRunAgainButRequiresSetup && isResetRunLoading - - let buttonText: string = '' - let handleButtonClick = (): void => {} - let buttonIconName: IconName | null = null - let disableReason = null - - if ( - currentRunId === runId && - (!isSetupComplete || isFixtureMismatch) && - !isValidRunAgain - ) { - disableReason = t('setup_incomplete') - } else if (isOtherRunCurrent) { - disableReason = t('shared:robot_is_busy') - } else if (isRobotOnWrongVersionOfSoftware) { - disableReason = t('shared:a_software_update_is_available') - } else if ( - isDoorOpen && - runStatus != null && - START_RUN_STATUSES.includes(runStatus) - ) { - disableReason = t('close_door') - } - - const shouldShowHSConfirm = - isHeaterShakerInProtocol && - !isHeaterShakerShaking && - (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_STOPPED) - - if (isProtocolAnalyzing) { - buttonIconName = 'ot-spinner' - buttonText = t('analyzing_on_robot') - } else if ( - runStatus === RUN_STATUS_RUNNING || - (runStatus != null && RECOVERY_STATUSES.includes(runStatus)) - ) { - buttonIconName = 'pause' - buttonText = t('pause_run') - handleButtonClick = (): void => { - pause() - trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_ACTION.PAUSE }) - } - } else if (runStatus === RUN_STATUS_STOP_REQUESTED) { - buttonIconName = 'ot-spinner' - buttonText = t('canceling_run') - } else if (runStatus != null && START_RUN_STATUSES.includes(runStatus)) { - buttonIconName = 'play' - buttonText = - runStatus === RUN_STATUS_IDLE ? t('start_run') : t('resume_run') - handleButtonClick = () => { - if (isHeaterShakerShaking && isHeaterShakerInProtocol) { - setShowIsShakingModal(true) - } else if ( - missingSetupSteps.length !== 0 && - (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_STOPPED) - ) { - confirmMissingSteps() - } else if (shouldShowHSConfirm) { - confirmAttachment() - } else { - play() - navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) - trackProtocolRunEvent({ - name: - runStatus === RUN_STATUS_IDLE - ? ANALYTICS_PROTOCOL_RUN_ACTION.START - : ANALYTICS_PROTOCOL_RUN_ACTION.RESUME, - properties: - runStatus === RUN_STATUS_IDLE && robotAnalyticsData != null - ? robotAnalyticsData - : {}, - }) - } - } - } else if (runStatus != null && RUN_AGAIN_STATUSES.includes(runStatus)) { - buttonIconName = runAgainWithSpinner ? 'ot-spinner' : 'play' - buttonText = t('run_again') - handleButtonClick = () => { - reset() - trackEvent({ - name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, - properties: { sourceLocation: 'RunRecordDetail', robotSerialNumber }, - }) - trackProtocolRunEvent({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.AGAIN, - }) - } - } - - return ( - <> - - {buttonIconName != null ? ( - - ) : null} - - {buttonText} - - - {disableReason != null && ( - - {disableReason} - - )} - {showIsShakingModal && - activeHeaterShaker != null && - isHeaterShakerInProtocol && - runId != null && ( - { - setShowIsShakingModal(false) - }} - module={activeHeaterShaker} - startRun={play} - /> - )} - {showHSConfirmationModal && ( - - )} - {showMissingStepsConfirmationModal && ( - { - shouldShowHSConfirm - ? confirmAttachment() - : handleProceedToRunClick() - }} - missingSteps={missingSetupSteps} - /> - )} - {} - - ) -} - -// TODO(jh 04-24-2024): Split TerminalRunBanner into a RunSuccessBanner and RunFailedBanner. -interface TerminalRunProps { - runStatus: RunStatus | null - handleClearClick: () => void - isClosingCurrentRun: boolean - setShowRunFailedModal: (showRunFailedModal: boolean) => void - commandErrorList?: RunCommandErrors - isResetRunLoading: boolean - isRunCurrent: boolean - cancelledWithoutRecovery: boolean - highestPriorityError?: RunError | null -} -function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null { - const { - runStatus, - handleClearClick, - isClosingCurrentRun, - setShowRunFailedModal, - commandErrorList, - highestPriorityError, - isResetRunLoading, - isRunCurrent, - cancelledWithoutRecovery, - } = props - const { t } = useTranslation('run_details') - const completedWithErrors = - commandErrorList?.data != null && commandErrorList.data.length > 0 - - const handleRunSuccessClick = (): void => { - handleClearClick() - } - - const handleFailedRunClick = (): void => { - // TODO(jh, 08-15-24): See TODO related to commandErrorList above. - if (commandErrorList == null) { - handleClearClick() - } - setShowRunFailedModal(true) - } - - const buildSuccessBanner = (): JSX.Element => { - return ( - - - {t('run_completed')} - - - ) - } - - const buildErrorBanner = (): JSX.Element => { - return ( - - - - {highestPriorityError != null - ? t('error_info', { - errorType: highestPriorityError?.errorType, - errorCode: highestPriorityError?.errorCode, - }) - : `${ - runStatus === RUN_STATUS_SUCCEEDED - ? t('run_completed_with_warnings') - : t('run_canceled_with_errors') - }`} - - - - {runStatus === RUN_STATUS_SUCCEEDED - ? t('view_warning_details') - : t('view_error_details')} - - - - ) - } - - if ( - runStatus === RUN_STATUS_SUCCEEDED && - isRunCurrent && - !isResetRunLoading && - !completedWithErrors - ) { - return buildSuccessBanner() - } - // TODO(jh, 08-14-24): Ideally, the backend never returns the "user cancelled a run" error and cancelledWithoutRecovery becomes unnecessary. - else if ( - !cancelledWithoutRecovery && - !isResetRunLoading && - (highestPriorityError != null || completedWithErrors) - ) { - return buildErrorBanner() - } else { - return null - } -} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/DisplayRunStatus.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/DisplayRunStatus.tsx new file mode 100644 index 00000000000..70b388ec3ca --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/DisplayRunStatus.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + COLORS, + SPACING, + Icon, + Flex, + StyledText, +} from '@opentrons/components' +import { RUN_STATUS_RUNNING } from '@opentrons/api-client' + +import type { RunStatus } from '@opentrons/api-client' + +interface DisplayRunStatusProps { + runStatus: RunStatus | null +} + +// Styles the run status copy. +export function DisplayRunStatus(props: DisplayRunStatusProps): JSX.Element { + const { t } = useTranslation('run_details') + return ( + + {props.runStatus === RUN_STATUS_RUNNING ? ( + + + + ) : null} + + {props.runStatus != null ? t(`status_${String(props.runStatus)}`) : ''} + + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolAnalysisErrorBanner.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx similarity index 95% rename from app/src/organisms/Devices/ProtocolRun/ProtocolAnalysisErrorBanner.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx index 965b2aee53d..9749ba2cef8 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolAnalysisErrorBanner.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx @@ -15,8 +15,8 @@ import { Modal, } from '@opentrons/components' -import { getTopPortalEl } from '../../../App/portal' -import { Banner } from '../../../atoms/Banner' +import { getTopPortalEl } from '../../../../../App/portal' +import { Banner } from '../../../../../atoms/Banner' import type { AnalysisError } from '@opentrons/shared-data' diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/TerminalRunBannerContainer.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/TerminalRunBannerContainer.tsx new file mode 100644 index 00000000000..0adc49a68d8 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/TerminalRunBannerContainer.tsx @@ -0,0 +1,165 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + SPACING, + TYPOGRAPHY, + JUSTIFY_SPACE_BETWEEN, + Flex, + StyledText, + Link, + ALIGN_CENTER, +} from '@opentrons/components' +import { RUN_STATUS_STOPPED, RUN_STATUS_SUCCEEDED } from '@opentrons/api-client' + +import { Banner } from '../../../../../atoms/Banner' +import { useCloseCurrentRun } from '../../../../ProtocolUpload/hooks' +import { useIsRunCurrent } from '../../../../../resources/runs' +import { useMostRecentRunId } from '../../../../ProtocolUpload/hooks/useMostRecentRunId' + +import type { RunHeaderBannerContainerProps } from '.' + +type TerminalBannerType = 'success' | 'error' | null + +// Determine which terminal banner to render, if any. +export function useTerminalRunBannerContainer({ + runId, + runStatus, + isResetRunLoading, + runErrors, + enteredER, +}: RunHeaderBannerContainerProps): TerminalBannerType { + const { highestPriorityError, commandErrorList } = runErrors + + const isRunCurrent = useIsRunCurrent(runId) + const mostRecentRunId = useMostRecentRunId() + + const isMostRecentRun = mostRecentRunId === runId + const cancelledWithoutRecovery = + !enteredER && runStatus === RUN_STATUS_STOPPED + const completedWithErrors = + (commandErrorList != null && commandErrorList.length > 0) || + highestPriorityError != null + + const showSuccessBanner = + runStatus === RUN_STATUS_SUCCEEDED && + isRunCurrent && + !isResetRunLoading && + !completedWithErrors + + // TODO(jh, 08-14-24): Ideally, the backend never returns the "user cancelled a run" error and + // cancelledWithoutRecovery becomes unnecessary. + const showErrorBanner = + isMostRecentRun && + !cancelledWithoutRecovery && + !isResetRunLoading && + completedWithErrors + + if (showSuccessBanner) { + return 'success' + } else if (showErrorBanner) { + return 'error' + } else { + return null + } +} + +interface TerminalRunBannerContainerProps + extends RunHeaderBannerContainerProps { + bannerType: TerminalBannerType +} + +// Contains all possible banners that render after the run reaches a terminal run status. +export function TerminalRunBannerContainer( + props: TerminalRunBannerContainerProps +): JSX.Element { + const { bannerType } = props + + switch (bannerType) { + case 'success': + return + case 'error': + return + default: + console.error('Handle banner cases explicitly.') + return
+ } +} + +function ProtocolRunSuccessBanner(): JSX.Element { + const { t } = useTranslation('run_details') + + const { closeCurrentRun, isClosingCurrentRun } = useCloseCurrentRun() + + const handleRunSuccessClick = (): void => { + closeCurrentRun() + } + + return ( + + + {t('run_completed')} + + + ) +} + +function ProtocolRunErrorBanner({ + runErrors, + runStatus, + runHeaderModalContainerUtils, +}: RunHeaderBannerContainerProps): JSX.Element { + const { t } = useTranslation('run_details') + + const { closeCurrentRun } = useCloseCurrentRun() + + const { highestPriorityError, commandErrorList } = runErrors + + const handleFailedRunClick = (): void => { + // TODO(jh, 08-15-24): Revisit the control flow here here after + // commandErrorList may be fetched for a non-current run. + if (commandErrorList == null) { + closeCurrentRun() + } + runHeaderModalContainerUtils.runFailedModalUtils.toggleModal() + } + + return ( + + + + {highestPriorityError != null + ? t('error_info', { + errorType: highestPriorityError?.errorType, + errorCode: highestPriorityError?.errorCode, + }) + : `${ + runStatus === RUN_STATUS_SUCCEEDED + ? t('run_completed_with_warnings') + : t('run_canceled_with_errors') + }`} + + + {runStatus === RUN_STATUS_SUCCEEDED + ? t('view_warning_details') + : t('view_error_details')} + + + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolAnalysisErrorBanner.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/__tests__/ProtocolAnalysisErrorBanner.test.tsx similarity index 90% rename from app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolAnalysisErrorBanner.test.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/__tests__/ProtocolAnalysisErrorBanner.test.tsx index d80ff0b44b3..cacf30ff864 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolAnalysisErrorBanner.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/__tests__/ProtocolAnalysisErrorBanner.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach } from 'vitest' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../../../__testing-utils__' +import { i18n } from '../../../../../../i18n' import { ProtocolAnalysisErrorBanner } from '../ProtocolAnalysisErrorBanner' const render = ( diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/getShowGenericRunHeaderBanners.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/getShowGenericRunHeaderBanners.ts new file mode 100644 index 00000000000..e21853738d7 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/getShowGenericRunHeaderBanners.ts @@ -0,0 +1,45 @@ +import { + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_STOPPED, +} from '@opentrons/api-client' + +import { isCancellableStatus } from '../utils' + +import type { RunHeaderBannerContainerProps } from '.' + +interface ShowGenericRunHeaderBannersParams { + runStatus: RunHeaderBannerContainerProps['runStatus'] + enteredER: RunHeaderBannerContainerProps['enteredER'] + isDoorOpen: boolean +} + +interface ShowGenericRunHeaderBannersResult { + showRunCanceledBanner: boolean + showDoorOpenDuringRunBanner: boolean + showDoorOpenBeforeRunBanner: boolean +} + +// Returns the "should render" scalar for all the generic Banner components used by ProtocolRunHeader. +export function getShowGenericRunHeaderBanners({ + runStatus, + isDoorOpen, + enteredER, +}: ShowGenericRunHeaderBannersParams): ShowGenericRunHeaderBannersResult { + const showRunCanceledBanner = runStatus === RUN_STATUS_STOPPED && !enteredER + + const showDoorOpenBeforeRunBanner = + isDoorOpen && + runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR && + runStatus !== RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR && + isCancellableStatus(runStatus) + + const showDoorOpenDuringRunBanner = + runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR + + return { + showRunCanceledBanner, + showDoorOpenBeforeRunBanner, + showDoorOpenDuringRunBanner, + } +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/index.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/index.tsx new file mode 100644 index 00000000000..80a78f288fc --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/index.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { Box, SPACING } from '@opentrons/components' + +import { ProtocolAnalysisErrorBanner } from './ProtocolAnalysisErrorBanner' +import { Banner } from '../../../../../atoms/Banner' +import { + TerminalRunBannerContainer, + useTerminalRunBannerContainer, +} from './TerminalRunBannerContainer' +import { getShowGenericRunHeaderBanners } from './getShowGenericRunHeaderBanners' +import { useIsDoorOpen } from '../hooks' + +import type { RunStatus } from '@opentrons/api-client' +import type { ProtocolRunHeaderProps } from '..' +import type { UseRunErrorsResult } from '../hooks' +import type { UseRunHeaderModalContainerResult } from '../RunHeaderModalContainer' + +export type RunHeaderBannerContainerProps = ProtocolRunHeaderProps & { + runStatus: RunStatus | null + enteredER: boolean + isResetRunLoading: boolean + runErrors: UseRunErrorsResult + runHeaderModalContainerUtils: UseRunHeaderModalContainerResult +} + +// Holds all the various banners that render in ProtocolRunHeader. +export function RunHeaderBannerContainer( + props: RunHeaderBannerContainerProps +): JSX.Element | null { + const { runStatus, enteredER, runHeaderModalContainerUtils } = props + const { analysisErrorModalUtils } = runHeaderModalContainerUtils + + const { t } = useTranslation(['run_details', 'shared']) + const isDoorOpen = useIsDoorOpen(props.robotName) + + const { + showRunCanceledBanner, + showDoorOpenBeforeRunBanner, + showDoorOpenDuringRunBanner, + } = getShowGenericRunHeaderBanners({ + runStatus, + isDoorOpen, + enteredER, + }) + + const terminalBannerType = useTerminalRunBannerContainer(props) + + return ( + + {analysisErrorModalUtils.showModal ? ( + + ) : null} + {showRunCanceledBanner ? ( + + {t('run_canceled')} + + ) : null} + {showDoorOpenBeforeRunBanner ? ( + + {t('shared:close_robot_door')} + + ) : null} + {showDoorOpenDuringRunBanner ? ( + + {t('close_door_to_resume')} + + ) : null} + {terminalBannerType != null ? ( + + ) : null} + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/index.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/index.ts new file mode 100644 index 00000000000..d89d78cea56 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/index.ts @@ -0,0 +1,3 @@ +export { useActionButtonProperties } from './useActionButtonProperties' +export { useActionBtnDisabledUtils } from './useActionBtnDisabledUtils' +export { useIsFixtureMismatch } from './useIsFixtureMismatch' diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionBtnDisabledUtils.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionBtnDisabledUtils.ts new file mode 100644 index 00000000000..1af46b835d6 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionBtnDisabledUtils.ts @@ -0,0 +1,116 @@ +import { useTranslation } from 'react-i18next' + +import { RUN_STATUS_BLOCKED_BY_OPEN_DOOR } from '@opentrons/api-client' + +import { useIsDoorOpen } from '../../../hooks' +import { useIsFixtureMismatch } from './useIsFixtureMismatch' +import { + isCancellableStatus, + isDisabledStatus, + isStartRunStatus, +} from '../../../utils' + +import type { BaseActionButtonProps } from '..' + +interface UseActionButtonDisabledUtilsProps extends BaseActionButtonProps { + isCurrentRun: boolean + isValidRunAgain: boolean + isSetupComplete: boolean + isOtherRunCurrent: boolean + isProtocolNotReady: boolean + isRobotOnWrongVersionOfSoftware: boolean +} + +type UseActionButtonDisabledUtilsResult = + | { isDisabled: true; disabledReason: string } + | { isDisabled: false; disabledReason: null } + +// Manages the various reasons the ActionButton may be disabled, returning the disabled state and user-facing disabled +// reason copy if applicable. +export function useActionBtnDisabledUtils( + props: UseActionButtonDisabledUtilsProps +): UseActionButtonDisabledUtilsResult { + const { + isCurrentRun, + isSetupComplete, + isOtherRunCurrent, + isProtocolNotReady, + runStatus, + isRobotOnWrongVersionOfSoftware, + protocolRunControls, + robotName, + runId, + isResetRunLoadingRef, + } = props + + const { t } = useTranslation('shared') + const { + isPlayRunActionLoading, + isPauseRunActionLoading, + } = protocolRunControls + const isDoorOpen = useIsDoorOpen(robotName) + const isFixtureMismatch = useIsFixtureMismatch(runId, robotName) + const isResetRunLoading = isResetRunLoadingRef.current + + const isDisabled = + (isCurrentRun && !isSetupComplete) || + isPlayRunActionLoading || + isPauseRunActionLoading || + isResetRunLoading || + isOtherRunCurrent || + isProtocolNotReady || + isFixtureMismatch || + isDisabledStatus(runStatus) || + isRobotOnWrongVersionOfSoftware || + (isDoorOpen && + runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR && + isCancellableStatus(runStatus)) + + const disabledReason = useDisabledReason({ + ...props, + isDoorOpen, + isFixtureMismatch, + isResetRunLoading, + }) + + return isDisabled + ? { isDisabled: true, disabledReason: disabledReason ?? t('robot_is_busy') } + : { isDisabled: false, disabledReason: null } +} + +type UseDisabledReasonProps = UseActionButtonDisabledUtilsProps & { + isDoorOpen: boolean + isFixtureMismatch: boolean + isResetRunLoading: boolean +} + +// The user-facing disabled explanation for why the ActionButton is disabled, if any. +function useDisabledReason({ + isCurrentRun, + isSetupComplete, + isFixtureMismatch, + isValidRunAgain, + isOtherRunCurrent, + isRobotOnWrongVersionOfSoftware, + isDoorOpen, + runStatus, + isResetRunLoading, +}: UseDisabledReasonProps): string | null { + const { t } = useTranslation(['run_details', 'shared']) + + if ( + isCurrentRun && + (!isSetupComplete || isFixtureMismatch) && + !isValidRunAgain + ) { + return t('setup_incomplete') + } else if (isOtherRunCurrent && !isResetRunLoading) { + return t('shared:robot_is_busy') + } else if (isRobotOnWrongVersionOfSoftware) { + return t('shared:a_software_update_is_available') + } else if (isDoorOpen && isStartRunStatus(runStatus)) { + return t('close_door') + } else { + return null + } +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts new file mode 100644 index 00000000000..540b1237333 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts @@ -0,0 +1,135 @@ +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' + +import { + RUN_STATUS_IDLE, + RUN_STATUS_RUNNING, + RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_STOPPED, +} from '@opentrons/api-client' + +import { + ANALYTICS_PROTOCOL_PROCEED_TO_RUN, + ANALYTICS_PROTOCOL_RUN_ACTION, + useTrackEvent, +} from '../../../../../../../redux/analytics' +import { useTrackProtocolRunEvent } from '../../../../../hooks' +import { useIsHeaterShakerInProtocol } from '../../../../../../ModuleCard/hooks' +import { isAnyHeaterShakerShaking } from '../../../RunHeaderModalContainer/modals' +import { + isRecoveryStatus, + isRunAgainStatus, + isStartRunStatus, +} from '../../../utils' + +import type { IconName } from '@opentrons/components' +import type { BaseActionButtonProps } from '..' + +interface UseButtonPropertiesProps extends BaseActionButtonProps { + isProtocolNotReady: boolean + confirmMissingSteps: () => void + confirmAttachment: () => void + robotAnalyticsData: any + robotSerialNumber: string + currentRunId: string | null + isValidRunAgain: boolean + isOtherRunCurrent: boolean + isRobotOnWrongVersionOfSoftware: boolean +} + +// Returns ActionButton properties. +export function useActionButtonProperties({ + isProtocolNotReady, + runStatus, + missingSetupSteps, + robotName, + runId, + confirmAttachment, + confirmMissingSteps, + robotAnalyticsData, + robotSerialNumber, + protocolRunControls, + attachedModules, + runHeaderModalContainerUtils, + isResetRunLoadingRef, +}: UseButtonPropertiesProps): { + buttonText: string + handleButtonClick: () => void + buttonIconName: IconName | null +} { + const { t } = useTranslation(['run_details', 'shared']) + const navigate = useNavigate() + const { play, pause, reset } = protocolRunControls + const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) + const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() + const isHeaterShakerShaking = isAnyHeaterShakerShaking(attachedModules) + const trackEvent = useTrackEvent() + + let buttonText = '' + let handleButtonClick = (): void => {} + let buttonIconName: IconName | null = null + + if (isProtocolNotReady) { + buttonIconName = 'ot-spinner' + buttonText = t('analyzing_on_robot') + } else if (runStatus === RUN_STATUS_RUNNING || isRecoveryStatus(runStatus)) { + buttonIconName = 'pause' + buttonText = t('pause_run') + handleButtonClick = () => { + pause() + trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_ACTION.PAUSE }) + } + } else if (runStatus === RUN_STATUS_STOP_REQUESTED) { + buttonIconName = 'ot-spinner' + buttonText = t('canceling_run') + } else if (isStartRunStatus(runStatus)) { + buttonIconName = 'play' + buttonText = + runStatus === RUN_STATUS_IDLE ? t('start_run') : t('resume_run') + handleButtonClick = () => { + if (isHeaterShakerShaking && isHeaterShakerInProtocol) { + runHeaderModalContainerUtils.HSRunningModalUtils.toggleModal?.() + } else if ( + missingSetupSteps.length !== 0 && + (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_STOPPED) + ) { + confirmMissingSteps() + } else if ( + isHeaterShakerInProtocol && + !isHeaterShakerShaking && + (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_STOPPED) + ) { + confirmAttachment() + } else { + play() + navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) + trackProtocolRunEvent({ + name: + runStatus === RUN_STATUS_IDLE + ? ANALYTICS_PROTOCOL_RUN_ACTION.START + : ANALYTICS_PROTOCOL_RUN_ACTION.RESUME, + properties: + runStatus === RUN_STATUS_IDLE && robotAnalyticsData != null + ? robotAnalyticsData + : {}, + }) + } + } + } else if (isRunAgainStatus(runStatus)) { + buttonIconName = isResetRunLoadingRef.current ? 'ot-spinner' : 'play' + buttonText = t('run_again') + handleButtonClick = () => { + isResetRunLoadingRef.current = true + reset() + trackEvent({ + name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, + properties: { sourceLocation: 'RunRecordDetail', robotSerialNumber }, + }) + trackProtocolRunEvent({ + name: ANALYTICS_PROTOCOL_RUN_ACTION.AGAIN, + }) + } + } + + return { buttonText, handleButtonClick, buttonIconName } +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useIsFixtureMismatch.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useIsFixtureMismatch.ts new file mode 100644 index 00000000000..92929566c77 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useIsFixtureMismatch.ts @@ -0,0 +1,20 @@ +import { useMostRecentCompletedAnalysis } from '../../../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useRobotType } from '../../../../../hooks' +import { + getIsFixtureMismatch, + useDeckConfigurationCompatibility, +} from '../../../../../../../resources/deck_configuration' + +export function useIsFixtureMismatch( + runId: string, + robotName: string +): boolean { + const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) + const robotType = useRobotType(robotName) + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + robotProtocolAnalysis + ) + + return getIsFixtureMismatch(deckConfigCompatibility) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/index.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/index.tsx new file mode 100644 index 00000000000..93132646f92 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/index.tsx @@ -0,0 +1,143 @@ +import * as React from 'react' + +import { RUN_STATUS_STOP_REQUESTED } from '@opentrons/api-client' +import { + ALIGN_CENTER, + DISPLAY_FLEX, + Icon, + JUSTIFY_CENTER, + PrimaryButton, + SIZE_1, + SPACING, + StyledText, + Tooltip, + useHoverTooltip, +} from '@opentrons/components' + +import { + useModuleCalibrationStatus, + useProtocolDetailsForRun, + useRobot, + useRobotAnalyticsData, + useRunCalibrationStatus, + useUnmatchedModulesForProtocol, +} from '../../../../hooks' +import { useCurrentRunId } from '../../../../../../resources/runs' +import { useActionBtnDisabledUtils, useActionButtonProperties } from './hooks' +import { getFallbackRobotSerialNumber, isRunAgainStatus } from '../../utils' +import { useIsRobotOnWrongVersionOfSoftware } from '../../../../../../redux/robot-update' + +import type { RunHeaderContentProps } from '..' + +export type BaseActionButtonProps = RunHeaderContentProps + +interface ActionButtonProps extends BaseActionButtonProps { + isResetRunLoadingRef: React.MutableRefObject +} + +export function ActionButton(props: ActionButtonProps): JSX.Element { + const { + runId, + robotName, + runStatus, + isResetRunLoadingRef, + runHeaderModalContainerUtils, + } = props + const { + missingStepsModalUtils, + HSConfirmationModalUtils, + } = runHeaderModalContainerUtils + + const [targetProps, tooltipProps] = useHoverTooltip() + const { isProtocolAnalyzing, protocolData } = useProtocolDetailsForRun(runId) + const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId) + const { complete: isCalibrationComplete } = useRunCalibrationStatus( + robotName, + runId + ) + const { complete: isModuleCalibrationComplete } = useModuleCalibrationStatus( + robotName, + runId + ) + const isRobotOnWrongVersionOfSoftware = useIsRobotOnWrongVersionOfSoftware( + robotName + ) + const currentRunId = useCurrentRunId() + + const isSetupComplete = + isCalibrationComplete && + isModuleCalibrationComplete && + missingModuleIds.length === 0 + const isCurrentRun = currentRunId === runId + const isOtherRunCurrent = currentRunId != null && currentRunId !== runId + const isProtocolNotReady = protocolData == null || !!isProtocolAnalyzing + const isValidRunAgain = isRunAgainStatus(runStatus) + + const { isDisabled, disabledReason } = useActionBtnDisabledUtils({ + isCurrentRun, + isSetupComplete, + isOtherRunCurrent, + isProtocolNotReady, + isRobotOnWrongVersionOfSoftware, + isValidRunAgain, + ...props, + }) + + const robot = useRobot(robotName) + const robotSerialNumber = getFallbackRobotSerialNumber(robot) + const robotAnalyticsData = useRobotAnalyticsData(robotName) + + const validRunAgainButRequiresSetup = isValidRunAgain && !isSetupComplete + + const { + buttonText, + handleButtonClick, + buttonIconName, + } = useActionButtonProperties({ + isProtocolNotReady, + confirmMissingSteps: missingStepsModalUtils.conditionalConfirmUtils.confirm, + confirmAttachment: HSConfirmationModalUtils.conditionalConfirmUtils.confirm, + robotAnalyticsData, + robotSerialNumber, + currentRunId, + isValidRunAgain, + isOtherRunCurrent, + isRobotOnWrongVersionOfSoftware, + ...props, + }) + + return ( + <> + + {buttonIconName != null ? ( + + ) : null} + {buttonText} + + {disabledReason && ( + + {disabledReason} + + )} + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/LabeledValue.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/LabeledValue.tsx new file mode 100644 index 00000000000..0d839adbc93 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/LabeledValue.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' + +import { + DIRECTION_COLUMN, + COLORS, + SPACING, + Flex, + StyledText, +} from '@opentrons/components' + +interface LabeledValueProps { + label: string + value: React.ReactNode +} + +export function LabeledValue(props: LabeledValueProps): JSX.Element { + return ( + + + {props.label} + + {typeof props.value === 'string' ? ( + {props.value} + ) : ( + props.value + )} + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/RunHeaderSectionLower.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/RunHeaderSectionLower.tsx new file mode 100644 index 00000000000..0b6d3062635 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/RunHeaderSectionLower.tsx @@ -0,0 +1,77 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' + +import { + BORDERS, + Box, + COLORS, + Flex, + JUSTIFY_FLEX_END, + SecondaryButton, + SPACING, +} from '@opentrons/components' +import { RUN_STATUS_RUNNING } from '@opentrons/api-client' + +import { formatTimestamp } from '../../../utils' +import { EMPTY_TIMESTAMP } from '../../../constants' +import { + useRunControls, + useRunTimestamps, +} from '../../../../RunTimeControl/hooks' +import { useCloseCurrentRun } from '../../../../ProtocolUpload/hooks' +import { LabeledValue } from './LabeledValue' +import { isCancellableStatus } from '../utils' + +import type { RunHeaderContentProps } from '.' + +// The lower row of Protocol Run Header. +export function RunHeaderSectionLower({ + runId, + runStatus, + runHeaderModalContainerUtils, +}: RunHeaderContentProps): JSX.Element { + const { t } = useTranslation('run_details') + + const { startedAt, completedAt } = useRunTimestamps(runId) + const { pause } = useRunControls(runId) + const { isClosingCurrentRun } = useCloseCurrentRun() + + const startedAtTimestamp = + startedAt != null ? formatTimestamp(startedAt) : EMPTY_TIMESTAMP + const completedAtTimestamp = + completedAt != null ? formatTimestamp(completedAt) : EMPTY_TIMESTAMP + + const handleCancelRunClick = (): void => { + if (runStatus === RUN_STATUS_RUNNING) { + pause() + } + runHeaderModalContainerUtils.confirmCancelModalUtils.toggleModal() + } + + return ( + + + + + {isCancellableStatus(runStatus) && ( + + {t('cancel_run')} + + )} + + + ) +} + +const SECTION_STYLE = css` + display: grid; + grid-template-columns: 4fr 6fr 4fr; + background-color: ${COLORS.grey10}; + padding: ${SPACING.spacing8}; + border-radius: ${BORDERS.borderRadius4}; +` diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/RunHeaderSectionUpper.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/RunHeaderSectionUpper.tsx new file mode 100644 index 00000000000..65428c7c9b8 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/RunHeaderSectionUpper.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' + +import { Box, Flex, JUSTIFY_FLEX_END } from '@opentrons/components' + +import { LabeledValue } from './LabeledValue' +import { DisplayRunStatus } from '../DisplayRunStatus' +import { RunTimer } from '../../RunTimer' +import { ActionButton } from './ActionButton' +import { useRunCreatedAtTimestamp } from '../../../hooks' +import { useRunTimestamps } from '../../../../RunTimeControl/hooks' + +import type { RunHeaderContentProps } from '.' + +// The upper row of Protocol Run Header. +export function RunHeaderSectionUpper( + props: RunHeaderContentProps +): JSX.Element { + const { runId, runStatus } = props + + const { t } = useTranslation('run_details') + + const createdAtTimestamp = useRunCreatedAtTimestamp(runId) + const { startedAt, stoppedAt, completedAt } = useRunTimestamps(runId) + + return ( + + + } + /> + + } + /> + + + + + ) +} + +const SECTION_STYLE = css` + display: grid; + grid-template-columns: 4fr 3fr 3fr 4fr; +` diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/index.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/index.tsx new file mode 100644 index 00000000000..bb5f1779ea3 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/index.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' + +import { RunHeaderSectionUpper } from './RunHeaderSectionUpper' +import { RunHeaderSectionLower } from './RunHeaderSectionLower' + +import type { ProtocolRunHeaderProps } from '..' +import type { AttachedModule, RunStatus } from '@opentrons/api-client' +import type { RunControls } from '../../../../RunTimeControl/hooks' +import type { UseRunHeaderModalContainerResult } from '../RunHeaderModalContainer' + +export type RunHeaderContentProps = ProtocolRunHeaderProps & { + runStatus: RunStatus | null + isResetRunLoadingRef: React.MutableRefObject + attachedModules: AttachedModule[] + protocolRunControls: RunControls + runHeaderModalContainerUtils: UseRunHeaderModalContainerResult +} + +export function RunHeaderContent(props: RunHeaderContentProps): JSX.Element { + return ( + <> + + {props.runStatus != null ? : null} + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/RunHeaderModalContainer.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/RunHeaderModalContainer.tsx new file mode 100644 index 00000000000..57d85a7e5c4 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/RunHeaderModalContainer.tsx @@ -0,0 +1,96 @@ +import * as React from 'react' + +import { ErrorRecoveryFlows } from '../../../../ErrorRecoveryFlows' +import { DropTipWizardFlows } from '../../../../DropTipWizardFlows' +import { useMostRecentCompletedAnalysis } from '../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { + ConfirmCancelModal, + HeaterShakerIsRunningModal, + ProtocolAnalysisErrorModal, + ProtocolDropTipModal, + RunFailedModal, + ConfirmMissingStepsModal, +} from './modals' +import { ConfirmAttachmentModal } from '../../../../ModuleCard/ConfirmAttachmentModal' + +import type { RunStatus } from '@opentrons/api-client' +import type { RunControls } from '../../../../RunTimeControl/hooks' +import type { UseRunErrorsResult } from '../hooks' +import type { UseRunHeaderModalContainerResult } from '.' + +export interface RunHeaderModalContainerProps { + runId: string + runStatus: RunStatus | null + robotName: string + protocolRunControls: RunControls + runHeaderModalContainerUtils: UseRunHeaderModalContainerResult + runErrors: UseRunErrorsResult +} + +// Contains all the various modals that render in ProtocolRunHeader. +export function RunHeaderModalContainer( + props: RunHeaderModalContainerProps +): JSX.Element | null { + const { runId, runStatus, runHeaderModalContainerUtils } = props + const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) + + const { + confirmCancelModalUtils, + analysisErrorModalUtils, + HSConfirmationModalUtils, + HSRunningModalUtils, + runFailedModalUtils, + recoveryModalUtils, + missingStepsModalUtils, + dropTipUtils, + } = runHeaderModalContainerUtils + const { dropTipModalUtils, dropTipWizardUtils } = dropTipUtils + + // TODO(jh, 09-10-24): Instead of having each modal be responsible for its own portal, do all the portaling here. + return ( + <> + {recoveryModalUtils.isERActive ? ( + + ) : null} + {runFailedModalUtils.showRunFailedModal ? ( + + ) : null} + {confirmCancelModalUtils.showModal ? ( + + ) : null} + {dropTipWizardUtils.showDTWiz ? ( + + ) : null} + {analysisErrorModalUtils.showModal ? ( + + ) : null} + {dropTipModalUtils.showModal ? ( + + ) : null} + {HSRunningModalUtils.showModal ? ( + + ) : null} + {HSConfirmationModalUtils.showModal && ( + + )} + {missingStepsModalUtils.showModal && ( + + )} + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/index.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/index.ts new file mode 100644 index 00000000000..3aac8c93a83 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './useHeaterShakerConfirmationModal' +export * from './useMissingStepsModal' +export * from './useRunHeaderDropTip' diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useHeaterShakerConfirmationModal.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useHeaterShakerConfirmationModal.ts new file mode 100644 index 00000000000..cf19abcc6f6 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useHeaterShakerConfirmationModal.ts @@ -0,0 +1,50 @@ +import { useSelector } from 'react-redux' + +import { useConditionalConfirm } from '@opentrons/components' + +import { getIsHeaterShakerAttached } from '../../../../../../redux/config' + +import type { UseConditionalConfirmResult } from '@opentrons/components' +import type { ConfirmAttachmentModalProps } from '../../../../../ModuleCard/ConfirmAttachmentModal' + +export type UseHeaterShakerConfirmationModalResult = + | { + showModal: true + modalProps: ConfirmAttachmentModalProps + conditionalConfirmUtils: UseConditionalConfirmResult<[]> + } + | { + showModal: false + modalProps: null + conditionalConfirmUtils: UseConditionalConfirmResult<[]> + } + +export function useHeaterShakerConfirmationModal( + handleProceedToRunClick: () => void +): UseHeaterShakerConfirmationModalResult { + const configBypassHeaterShakerAttachmentConfirmation = useSelector( + getIsHeaterShakerAttached + ) + const conditionalConfirmUtils = useConditionalConfirm( + handleProceedToRunClick, + !configBypassHeaterShakerAttachmentConfirmation + ) + + const modalProps: ConfirmAttachmentModalProps = { + onCloseClick: conditionalConfirmUtils.cancel, + isProceedToRunModal: true, + onConfirmClick: conditionalConfirmUtils.confirm, + } + + return conditionalConfirmUtils.showConfirmation + ? { + showModal: true, + modalProps, + conditionalConfirmUtils, + } + : { + showModal: false, + modalProps: null, + conditionalConfirmUtils, + } +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts new file mode 100644 index 00000000000..a5d91e472ba --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts @@ -0,0 +1,66 @@ +import { RUN_STATUS_IDLE, RUN_STATUS_STOPPED } from '@opentrons/api-client' +import { useConditionalConfirm } from '@opentrons/components' + +import { useIsHeaterShakerInProtocol } from '../../../../../ModuleCard/hooks' +import { isAnyHeaterShakerShaking } from '../modals' + +import type { UseConditionalConfirmResult } from '@opentrons/components' +import type { RunStatus, AttachedModule } from '@opentrons/api-client' +import type { ConfirmMissingStepsModalProps } from '../modals' + +interface UseMissingStepsModalProps { + runStatus: RunStatus | null + attachedModules: AttachedModule[] + missingSetupSteps: string[] + handleProceedToRunClick: () => void +} + +export type UseMissingStepsModalResult = + | { + showModal: true + modalProps: ConfirmMissingStepsModalProps + conditionalConfirmUtils: UseConditionalConfirmResult<[]> + } + | { + showModal: false + modalProps: null + conditionalConfirmUtils: UseConditionalConfirmResult<[]> + } + +export function useMissingStepsModal({ + attachedModules, + runStatus, + missingSetupSteps, + handleProceedToRunClick, +}: UseMissingStepsModalProps): UseMissingStepsModalResult { + const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() + const isHeaterShakerShaking = isAnyHeaterShakerShaking(attachedModules) + + const shouldShowHSConfirm = + isHeaterShakerInProtocol && + !isHeaterShakerShaking && + (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_STOPPED) + + const conditionalConfirmUtils = useConditionalConfirm( + handleProceedToRunClick, + missingSetupSteps.length !== 0 + ) + + const modalProps: ConfirmMissingStepsModalProps = { + onCloseClick: conditionalConfirmUtils.cancel, + onConfirmClick: () => { + shouldShowHSConfirm + ? conditionalConfirmUtils.confirm() + : handleProceedToRunClick() + }, + missingSteps: missingSetupSteps, + } + + return conditionalConfirmUtils.showConfirmation + ? { + showModal: true, + modalProps, + conditionalConfirmUtils, + } + : { showModal: false, modalProps: null, conditionalConfirmUtils } +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts new file mode 100644 index 00000000000..013ae24f3aa --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts @@ -0,0 +1,149 @@ +import * as React from 'react' + +import { useHost } from '@opentrons/react-api-client' +import { RUN_STATUS_IDLE, RUN_STATUS_STOPPED } from '@opentrons/api-client' +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' + +import { + useDropTipWizardFlows, + useTipAttachmentStatus, +} from '../../../../../DropTipWizardFlows' +import { useProtocolDropTipModal } from '../modals' +import { useCloseCurrentRun } from '../../../../../ProtocolUpload/hooks' +import { useIsRunCurrent } from '../../../../../../resources/runs' +import { isTerminalRunStatus } from '../../utils' + +import type { RobotType } from '@opentrons/shared-data' +import type { Run, RunStatus } from '@opentrons/api-client' +import type { + DropTipWizardFlowsProps, + PipetteWithTip, +} from '../../../../../DropTipWizardFlows' +import type { UseProtocolDropTipModalResult } from '../modals' +import type { PipetteDetails } from '../../../../../../resources/maintenance_runs' + +export type RunHeaderDropTipWizProps = + | { showDTWiz: true; dtWizProps: DropTipWizardFlowsProps } + | { showDTWiz: false; dtWizProps: null } + +export interface UseRunHeaderDropTipParams { + runId: string + runRecord: Run | null + robotType: RobotType + runStatus: RunStatus | null +} + +export interface UseRunHeaderDropTipResult { + dropTipModalUtils: UseProtocolDropTipModalResult + dropTipWizardUtils: RunHeaderDropTipWizProps +} + +// Handles all the tip related logic during a protocol run on the desktop app. +export function useRunHeaderDropTip({ + runId, + runRecord, + robotType, + runStatus, +}: UseRunHeaderDropTipParams): UseRunHeaderDropTipResult { + const host = useHost() + const isRunCurrent = useIsRunCurrent(runId) + const enteredER = runRecord?.data.hasEverEnteredErrorRecovery ?? false + + const { closeCurrentRun } = useCloseCurrentRun() + const { showDTWiz, toggleDTWiz } = useDropTipWizardFlows() + + const { + areTipsAttached, + determineTipStatus, + resetTipStatus, + setTipStatusResolved, + aPipetteWithTip, + initialPipettesWithTipsCount, + } = useTipAttachmentStatus({ + runId, + runRecord: runRecord ?? null, + host, + }) + + const dropTipModalUtils = useProtocolDropTipModal({ + areTipsAttached, + toggleDTWiz, + isRunCurrent, + currentRunId: runId, + pipetteInfo: buildPipetteDetails(aPipetteWithTip), + onSkipAndHome: () => { + closeCurrentRun() + }, + }) + + // The onCloseFlow for Drop Tip Wizard + const onCloseFlow = (isTakeover?: boolean): void => { + if (isTakeover) { + toggleDTWiz() + } else { + void setTipStatusResolved(() => { + toggleDTWiz() + closeCurrentRun() + }, toggleDTWiz) + } + } + + const buildDTWizUtils = (): RunHeaderDropTipWizProps => { + return showDTWiz && aPipetteWithTip != null + ? { + showDTWiz: true, + dtWizProps: { + robotType, + mount: aPipetteWithTip.mount, + instrumentModelSpecs: aPipetteWithTip.specs, + closeFlow: onCloseFlow, + }, + } + : { showDTWiz: false, dtWizProps: null } + } + + // Manage tip checking + React.useEffect(() => { + // If a user begins a new run without navigating away from the run page, reset tip status. + if (robotType === FLEX_ROBOT_TYPE) { + if (runStatus === RUN_STATUS_IDLE) { + resetTipStatus() + } + // Only determine tip status when necessary as this can be an expensive operation. Error Recovery handles tips, so don't + // have to do it here if done during Error Recovery. + else if (isTerminalRunStatus(runStatus) && !enteredER) { + void determineTipStatus() + } + } + }, [runStatus, robotType, enteredER]) + + // TODO(jh, 08-15-24): The enteredER condition is a hack, because errorCommands are only returned when a run is current. + // Ideally the run should not need to be current to view errorCommands. + + // If the run terminates with a "stopped" status, close the run if no tips are attached after running tip check at least once. + // This marks the robot as "not busy" if drop tip CTAs are unnecessary. + React.useEffect(() => { + if ( + runStatus === RUN_STATUS_STOPPED && + isRunCurrent && + (initialPipettesWithTipsCount === 0 || robotType === OT2_ROBOT_TYPE) && + !enteredER + ) { + closeCurrentRun() + } + }, [runStatus, isRunCurrent, enteredER, initialPipettesWithTipsCount]) + + return { dropTipModalUtils, dropTipWizardUtils: buildDTWizUtils() } +} + +// TODO(jh, 09-12-24): Consolidate this with the same utility that exists elsewhere. +function buildPipetteDetails( + aPipetteWithTip: PipetteWithTip | null +): PipetteDetails | null { + return aPipetteWithTip != null + ? { + pipetteId: aPipetteWithTip.specs.name, + mount: aPipetteWithTip.mount, + } + : null +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/index.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/index.ts new file mode 100644 index 00000000000..d26b6f2bfa6 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/index.ts @@ -0,0 +1,2 @@ +export * from './RunHeaderModalContainer' +export * from './useRunHeaderModalContainer' diff --git a/app/src/organisms/RunDetails/ConfirmCancelModal.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx similarity index 80% rename from app/src/organisms/RunDetails/ConfirmCancelModal.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx index 98bb3618925..0aa3781277c 100644 --- a/app/src/organisms/RunDetails/ConfirmCancelModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' + import { AlertPrimaryButton, ALIGN_CENTER, @@ -21,26 +22,42 @@ import { } from '@opentrons/api-client' import { useStopRunMutation } from '@opentrons/react-api-client' -import { getTopPortalEl } from '../../App/portal' -import { useTrackProtocolRunEvent, useIsFlex } from '../Devices/hooks' -import { useRunStatus } from '../RunTimeControl/hooks' -import { ANALYTICS_PROTOCOL_RUN_ACTION } from '../../redux/analytics' +import { getTopPortalEl } from '../../../../../../App/portal' +import { useTrackProtocolRunEvent, useIsFlex } from '../../../../hooks' +import { ANALYTICS_PROTOCOL_RUN_ACTION } from '../../../../../../redux/analytics' + +import type { RunStatus } from '@opentrons/api-client' + +export interface UseConfirmCancelModalResult { + showModal: boolean + toggleModal: () => void +} + +export function useConfirmCancelModal(): UseConfirmCancelModalResult { + const [showModal, setShowModal] = React.useState(false) + + const toggleModal = (): void => { + setShowModal(!showModal) + } + + return { showModal, toggleModal } +} export interface ConfirmCancelModalProps { onClose: () => unknown runId: string robotName: string + runStatus: RunStatus | null } export function ConfirmCancelModal( props: ConfirmCancelModalProps ): JSX.Element { - const { onClose, runId, robotName } = props + const { onClose, runId, robotName, runStatus } = props const { stopRun } = useStopRunMutation() - const [isCanceling, setIsCanceling] = React.useState(false) - const runStatus = useRunStatus(runId) const isFlex = useIsFlex(robotName) const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) + const [isCanceling, setIsCanceling] = React.useState(false) const { t } = useTranslation('run_details') const cancelRunAlertInfo = isFlex diff --git a/app/src/organisms/Devices/ProtocolRun/ConfirmMissingStepsModal.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx similarity index 95% rename from app/src/organisms/Devices/ProtocolRun/ConfirmMissingStepsModal.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx index f4749aa06d1..708f07b3789 100644 --- a/app/src/organisms/Devices/ProtocolRun/ConfirmMissingStepsModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx @@ -14,7 +14,7 @@ import { Modal, } from '@opentrons/components' -interface ConfirmMissingStepsModalProps { +export interface ConfirmMissingStepsModalProps { onCloseClick: () => void onConfirmClick: () => void missingSteps: string[] @@ -48,7 +48,7 @@ export const ConfirmMissingStepsModal = ( void } + | { showModal: false; module: null; toggleModal: null } + +export function useHeaterShakerIsRunningModal( + attachedModules: AttachedModule[] +): UseHeaterShakerIsRunningModalResult { + const [showIsShakingModal, setShowIsShakingModal] = React.useState(false) + + const activeHeaterShaker = getActiveHeaterShaker(attachedModules) + const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() + + const toggleModal = (): void => { + setShowIsShakingModal(!showIsShakingModal) + } + + const showModal = + showIsShakingModal && activeHeaterShaker != null && isHeaterShakerInProtocol + + return showModal + ? { + showModal: true, + module: activeHeaterShaker, + toggleModal, + } + : { showModal: false, module: null, toggleModal: null } +} interface HeaterShakerIsRunningModalProps { closeModal: () => void diff --git a/app/src/organisms/Devices/HeaterShakerWizard/HeaterShakerModuleCard.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/HeaterShakerModuleCard.tsx similarity index 88% rename from app/src/organisms/Devices/HeaterShakerWizard/HeaterShakerModuleCard.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/HeaterShakerModuleCard.tsx index 5b6f950a906..897a52fc445 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/HeaterShakerModuleCard.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/HeaterShakerModuleCard.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' + import { ALIGN_FLEX_START, COLORS, @@ -13,10 +14,11 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { getModuleDisplayName } from '@opentrons/shared-data' -import heaterShakerModule from '../../../assets/images/heater_shaker_module_transparent.png' -import { HeaterShakerModuleData } from '../../ModuleCard/HeaterShakerModuleData' -import type { HeaterShakerModule } from '../../../redux/modules/types' +import heaterShakerModule from '../../../../../../../assets/images/heater_shaker_module_transparent.png' +import { HeaterShakerModuleData } from '../../../../../../ModuleCard/HeaterShakerModuleData' + +import type { HeaterShakerModule } from '../../../../../../../redux/modules/types' interface HeaterShakerModuleCardProps { module: HeaterShakerModule diff --git a/app/src/organisms/Devices/__tests__/HeaterShakerIsRunningModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerIsRunningModal.test.tsx similarity index 89% rename from app/src/organisms/Devices/__tests__/HeaterShakerIsRunningModal.test.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerIsRunningModal.test.tsx index b447ad26ee5..9395d55b898 100644 --- a/app/src/organisms/Devices/__tests__/HeaterShakerIsRunningModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerIsRunningModal.test.tsx @@ -1,16 +1,19 @@ import * as React from 'react' -import { i18n } from '../../../i18n' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' -import '@testing-library/jest-dom/vitest' -import { renderWithProviders } from '../../../__testing-utils__' + import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' -import { mockHeaterShaker } from '../../../redux/modules/__fixtures__' + +import { i18n } from '../../../../../../../../i18n' +import { renderWithProviders } from '../../../../../../../../__testing-utils__' +import { mockHeaterShaker } from '../../../../../../../../redux/modules/__fixtures__' import { HeaterShakerIsRunningModal } from '../HeaterShakerIsRunningModal' -import { HeaterShakerModuleCard } from '../HeaterShakerWizard/HeaterShakerModuleCard' -import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useAttachedModules } from '../hooks' +import { HeaterShakerModuleCard } from '../HeaterShakerModuleCard' +import { useAttachedModules } from '../../../../../../hooks' +import { useMostRecentCompletedAnalysis } from '../../../../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' + import type * as ReactApiClient from '@opentrons/react-api-client' + vi.mock('@opentrons/react-api-client', async importOriginal => { const actual = await importOriginal() return { @@ -18,9 +21,11 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { useCreateLiveCommandMutation: vi.fn(), } }) -vi.mock('../hooks') -vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') -vi.mock('../HeaterShakerWizard/HeaterShakerModuleCard') +vi.mock('../../../../../../hooks') +vi.mock( + '../../../../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +) +vi.mock('../HeaterShakerModuleCard') const mockMovingHeaterShakerOne = { id: 'heatershaker_id_1', diff --git a/app/src/organisms/Devices/HeaterShakerWizard/__tests__/HeaterShakerModuleCard.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerModuleCard.test.tsx similarity index 71% rename from app/src/organisms/Devices/HeaterShakerWizard/__tests__/HeaterShakerModuleCard.test.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerModuleCard.test.tsx index c8fec9076dd..5484f2e5ec4 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/__tests__/HeaterShakerModuleCard.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerModuleCard.test.tsx @@ -1,14 +1,14 @@ import * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach } from 'vitest' -import '@testing-library/jest-dom/vitest' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' + +import { renderWithProviders } from '../../../../../../../../__testing-utils__' +import { mockHeaterShaker } from '../../../../../../../../redux/modules/__fixtures__' +import { i18n } from '../../../../../../../../i18n' import { HeaterShakerModuleCard } from '../HeaterShakerModuleCard' -import { HeaterShakerModuleData } from '../../../ModuleCard/HeaterShakerModuleData' -import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' +import { HeaterShakerModuleData } from '../../../../../../../ModuleCard/HeaterShakerModuleData' -vi.mock('../../../ModuleCard/HeaterShakerModuleData') +vi.mock('../../../../../../../ModuleCard/HeaterShakerModuleData') const render = (props: React.ComponentProps) => { return renderWithProviders(, { diff --git a/app/src/organisms/Devices/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx similarity index 92% rename from app/src/organisms/Devices/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx index 969d3d1afea..ae95b1f773b 100644 --- a/app/src/organisms/Devices/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx @@ -1,19 +1,21 @@ import * as React from 'react' import { Provider } from 'react-redux' import { describe, it, vi, beforeEach, expect } from 'vitest' -import '@testing-library/jest-dom/vitest' import { createStore } from 'redux' import { renderHook } from '@testing-library/react' + import { HEATERSHAKER_MODULE_V1 } from '@opentrons/shared-data' -import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' + +import { RUN_ID_1 } from '../../../../../../../RunTimeControl/__fixtures__' +import { useMostRecentCompletedAnalysis } from '../../../../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useHeaterShakerModuleIdsFromRun } from '../hooks' -import { RUN_ID_1 } from '../../../RunTimeControl/__fixtures__' import type { Store } from 'redux' -import type { State } from '../../../../redux/types' +import type { State } from '../../../../../../../../redux/types' -vi.mock('../../hooks') -vi.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') +vi.mock( + '../../../../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +) describe('useHeaterShakerModuleIdsFromRun', () => { const store: Store = createStore(vi.fn(), {}) diff --git a/app/src/organisms/Devices/HeaterShakerIsRunningModal/hooks.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/hooks.tsx similarity index 82% rename from app/src/organisms/Devices/HeaterShakerIsRunningModal/hooks.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/hooks.tsx index ee391e8c01c..680077a1aa6 100644 --- a/app/src/organisms/Devices/HeaterShakerIsRunningModal/hooks.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/hooks.tsx @@ -1,4 +1,4 @@ -import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useMostRecentCompletedAnalysis } from '../../../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' export interface ModuleIdsFromRun { moduleIdsFromRun: string[] diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/index.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/index.ts new file mode 100644 index 00000000000..46ea32198c8 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/index.ts @@ -0,0 +1,2 @@ +export * from './HeaterShakerIsRunningModal' +export { isAnyHeaterShakerShaking } from './utils' diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/utils.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/utils.ts new file mode 100644 index 00000000000..5936cf635c0 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/utils.ts @@ -0,0 +1,24 @@ +import type { HeaterShakerModule } from '../../../../../../../redux/modules/types' +import type { AttachedModule } from '@opentrons/api-client' + +export function getActiveHeaterShaker( + attachedModules: any[] +): HeaterShakerModule | undefined { + return attachedModules.find( + (module): module is HeaterShakerModule => + module.moduleType === 'heaterShakerModuleType' && + module?.data != null && + module.data.speedStatus !== 'idle' + ) +} + +export function isAnyHeaterShakerShaking( + attachedModules: AttachedModule[] +): boolean { + return attachedModules + .filter( + (module): module is HeaterShakerModule => + module.moduleType === 'heaterShakerModuleType' + ) + .some(module => module?.data != null && module.data.speedStatus !== 'idle') +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolAnalysisErrorModal.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolAnalysisErrorModal.tsx similarity index 56% rename from app/src/organisms/Devices/ProtocolRun/ProtocolAnalysisErrorModal.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolAnalysisErrorModal.tsx index 6ad5d5af302..fbf911f229d 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolAnalysisErrorModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolAnalysisErrorModal.tsx @@ -13,14 +13,56 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { getTopPortalEl } from '../../../App/portal' +import { getTopPortalEl } from '../../../../../../App/portal' +import { useProtocolAnalysisErrors } from '../../../../hooks' import type { AnalysisError } from '@opentrons/shared-data' -interface ProtocolAnalysisErrorModalProps { +export type UseAnalysisErrorsModalProps = Omit< + ProtocolAnalysisErrorModalProps, + 'errors' | 'onClose' +> & { runId: string | null } + +export type UseAnalysisErrorsModalResult = + | { showModal: false; modalProps: null } + | { showModal: true; modalProps: ProtocolAnalysisErrorModalProps } + +// Provides validated modal props. Implicitly set the modal to true if analysis errors are present. +export function useProtocolAnalysisErrorsModal({ + robotName, + displayName, + runId, +}: UseAnalysisErrorsModalProps): UseAnalysisErrorsModalResult { + const { analysisErrors } = useProtocolAnalysisErrors(runId) + const [showModal, setShowModal] = React.useState(false) + + React.useEffect(() => { + if (analysisErrors != null && analysisErrors?.length > 0) { + setShowModal(true) + } + }, [analysisErrors]) + + const toggleModal = (): void => { + setShowModal(false) + } + + return showModal && analysisErrors != null && analysisErrors.length > 0 + ? { + showModal: true, + modalProps: { + onClose: toggleModal, + errors: analysisErrors, + robotName, + displayName, + }, + } + : { showModal: false, modalProps: null } +} + +export interface ProtocolAnalysisErrorModalProps { displayName: string | null errors: AnalysisError[] - onClose: React.MouseEventHandler + onClose: () => void robotName: string } diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolDropTipModal.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx similarity index 67% rename from app/src/organisms/Devices/ProtocolRun/ProtocolDropTipModal.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx index f367af7412f..68e6c3601b6 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolDropTipModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx @@ -15,17 +15,19 @@ import { ModalShell, } from '@opentrons/components' -import { TextOnlyButton } from '../../../atoms/buttons' -import { useHomePipettes } from '../../DropTipWizardFlows/hooks' +import { TextOnlyButton } from '../../../../../../atoms/buttons' +import { useHomePipettes } from '../../../../../DropTipWizardFlows' import type { PipetteData } from '@opentrons/api-client' import type { IconProps } from '@opentrons/components' -import type { UseHomePipettesProps } from '../../DropTipWizardFlows/hooks' -import type { TipAttachmentStatusResult } from '../../DropTipWizardFlows' +import type { + UseHomePipettesProps, + TipAttachmentStatusResult, +} from '../../../../../DropTipWizardFlows' type UseProtocolDropTipModalProps = Pick< UseHomePipettesProps, - 'robotType' | 'instrumentModelSpecs' | 'mount' + 'pipetteInfo' > & { areTipsAttached: TipAttachmentStatusResult['areTipsAttached'] toggleDTWiz: () => void @@ -35,12 +37,12 @@ type UseProtocolDropTipModalProps = Pick< isRunCurrent: boolean } -interface UseProtocolDropTipModalResult { - showDTModal: boolean - onDTModalSkip: () => void - onDTModalRemoval: () => void - isDisabled: boolean -} +export type UseProtocolDropTipModalResult = + | { + showModal: true + modalProps: ProtocolDropTipModalProps + } + | { showModal: false; modalProps: null } // Wraps functionality required for rendering the related modal. export function useProtocolDropTipModal({ @@ -48,49 +50,51 @@ export function useProtocolDropTipModal({ toggleDTWiz, isRunCurrent, onSkipAndHome, - ...homePipetteProps + pipetteInfo, }: UseProtocolDropTipModalProps): UseProtocolDropTipModalResult { - const [showDTModal, setShowDTModal] = React.useState(areTipsAttached) + const [showModal, setShowModal] = React.useState(areTipsAttached) - const { homePipettes, isHomingPipettes } = useHomePipettes({ - ...homePipetteProps, - isRunCurrent, - onHome: () => { + const { homePipettes, isHoming } = useHomePipettes({ + pipetteInfo, + onSettled: () => { onSkipAndHome() - setShowDTModal(false) }, }) // Close the modal if a different app closes the run context. React.useEffect(() => { - if (isRunCurrent && !isHomingPipettes) { - setShowDTModal(areTipsAttached) + if (isRunCurrent && !isHoming) { + setShowModal(areTipsAttached) } else if (!isRunCurrent) { - setShowDTModal(false) + setShowModal(false) } - }, [isRunCurrent, areTipsAttached, showDTModal]) // Continue to show the modal if a client dismisses the maintenance run on a different app. + }, [isRunCurrent, areTipsAttached, showModal]) // Continue to show the modal if a client dismisses the maintenance run on a different app. - const onDTModalSkip = (): void => { - homePipettes() + const onSkip = (): void => { + void homePipettes() } - const onDTModalRemoval = (): void => { + const onBeginRemoval = (): void => { toggleDTWiz() - setShowDTModal(false) + setShowModal(false) } - return { - showDTModal, - onDTModalSkip, - onDTModalRemoval, - isDisabled: isHomingPipettes, - } + return showModal + ? { + showModal: true, + modalProps: { + onSkip, + onBeginRemoval, + isDisabled: isHoming, + }, + } + : { showModal: false, modalProps: null } } interface ProtocolDropTipModalProps { - onSkip: UseProtocolDropTipModalResult['onDTModalSkip'] - onBeginRemoval: UseProtocolDropTipModalResult['onDTModalRemoval'] - isDisabled: UseProtocolDropTipModalResult['isDisabled'] + onSkip: () => void + onBeginRemoval: () => void + isDisabled: boolean mount?: PipetteData['mount'] } diff --git a/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx similarity index 78% rename from app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx index 0ce8e2c6e50..77041e48c18 100644 --- a/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx @@ -22,57 +22,68 @@ import { DISPLAY_FLEX, } from '@opentrons/components' -import { useDownloadRunLog } from '../hooks' +import { useDownloadRunLog } from '../../../../hooks' import { RUN_STATUS_SUCCEEDED } from '@opentrons/api-client' -import type { - RunError, - RunCommandErrors, - RunStatus, -} from '@opentrons/api-client' +import type { RunStatus } from '@opentrons/api-client' import type { ModalProps } from '@opentrons/components' import type { RunCommandError } from '@opentrons/shared-data' +import type { UseRunErrorsResult } from '../../hooks' -/** - * This modal is for Desktop app - * @param robotName - Robot name - * @param runId - Run protocol id - * @param setShowRunFailedModal - For closing modal - * @param highestPriorityError - Run error information - * - * @returns JSX.Element | null - */ // Note(kk:08/07/2023) // This modal and run failed modal for Touchscreen app will be merged into one component like EstopModals. +export interface UseRunFailedModalResult { + showRunFailedModal: boolean + toggleModal: () => void +} + +export function useRunFailedModal( + runErrors: UseRunErrorsResult +): UseRunFailedModalResult { + const [showRunFailedModal, setShowRunFailedModal] = React.useState(false) + + const toggleModal = (): void => { + setShowRunFailedModal(!showRunFailedModal) + } + + const showModal = + showRunFailedModal && + (runErrors.commandErrorList != null || + runErrors.highestPriorityError != null) + + return { showRunFailedModal: showModal, toggleModal } +} + interface RunFailedModalProps { robotName: string runId: string - setShowRunFailedModal: (showRunFailedModal: boolean) => void - highestPriorityError?: RunError | null - commandErrorList?: RunCommandErrors | null + toggleModal: () => void runStatus: RunStatus | null + runErrors: UseRunErrorsResult } +// TODO(jh, 09-09-24): Consider cleaning up component after the server-side commandErrorList changes are completed. export function RunFailedModal({ robotName, runId, - setShowRunFailedModal, - highestPriorityError, - commandErrorList, + toggleModal, runStatus, + runErrors, }: RunFailedModalProps): JSX.Element | null { + const { commandErrorList, highestPriorityError } = runErrors + const { i18n, t } = useTranslation(['run_details', 'shared', 'branded']) const modalProps: ModalProps = { type: runStatus === RUN_STATUS_SUCCEEDED ? 'warning' : 'error', title: - commandErrorList == null || commandErrorList?.data.length === 0 + commandErrorList == null || commandErrorList?.length === 0 ? t('run_failed_modal_title') : runStatus === RUN_STATUS_SUCCEEDED ? t('warning_details') : t('error_details'), onClose: () => { - setShowRunFailedModal(false) + toggleModal() }, closeOnOutsideClick: true, childrenPadding: SPACING.spacing24, @@ -80,10 +91,8 @@ export function RunFailedModal({ } const { downloadRunLog } = useDownloadRunLog(robotName, runId) - if (highestPriorityError == null && commandErrorList == null) return null - const handleClick = (): void => { - setShowRunFailedModal(false) + toggleModal() } const handleDownloadClick: React.MouseEventHandler = e => { @@ -140,10 +149,10 @@ export function RunFailedModal({ 0 - ? commandErrorList?.data + : commandErrorList != null && commandErrorList.length > 0 + ? commandErrorList : [] } isSingleError={!!highestPriorityError} diff --git a/app/src/organisms/RunDetails/__fixtures__/analysis.json b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__fixtures__/analysis.json similarity index 100% rename from app/src/organisms/RunDetails/__fixtures__/analysis.json rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__fixtures__/analysis.json diff --git a/app/src/organisms/RunDetails/__fixtures__/runRecord.json b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__fixtures__/runRecord.json similarity index 100% rename from app/src/organisms/RunDetails/__fixtures__/runRecord.json rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__fixtures__/runRecord.json diff --git a/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ConfirmCancelModal.test.tsx similarity index 80% rename from app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ConfirmCancelModal.test.tsx index 92fed1f5e4f..e62c0a409c2 100644 --- a/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ConfirmCancelModal.test.tsx @@ -10,15 +10,12 @@ import { } from '@opentrons/api-client' import { useStopRunMutation } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' -import { - useIsFlex, - useTrackProtocolRunEvent, -} from '../../../organisms/Devices/hooks' -import { useTrackEvent } from '../../../redux/analytics' -import { renderWithProviders } from '../../../__testing-utils__' -import { ConfirmCancelModal } from '../../../organisms/RunDetails/ConfirmCancelModal' -import { useRunStatus } from '../../RunTimeControl/hooks' +import { i18n } from '../../../../../../../i18n' +import { renderWithProviders } from '../../../../../../../__testing-utils__' +import { useIsFlex, useTrackProtocolRunEvent } from '../../../../../hooks' +import { useTrackEvent } from '../../../../../../../redux/analytics' +import { ConfirmCancelModal } from '../ConfirmCancelModal' + import type * as ApiClient from '@opentrons/react-api-client' vi.mock('@opentrons/react-api-client', async importOriginal => { @@ -28,10 +25,8 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { useStopRunMutation: vi.fn(), } }) -vi.mock('../../RunTimeControl/hooks') -vi.mock('../../../organisms/Devices/hooks') -vi.mock('../../../redux/analytics') -vi.mock('../../../redux/config') +vi.mock('../../../../../hooks') +vi.mock('../../../../../../../redux/analytics') const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -54,14 +49,18 @@ describe('ConfirmCancelModal', () => { vi.mocked(useStopRunMutation).mockReturnValue({ stopRun: mockStopRun, } as any) - vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_RUNNING) vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) when(useTrackProtocolRunEvent).calledWith(RUN_ID, ROBOT_NAME).thenReturn({ trackProtocolRunEvent: mockTrackProtocolRunEvent, }) vi.mocked(useIsFlex).mockReturnValue(true) - props = { onClose: vi.fn(), runId: RUN_ID, robotName: ROBOT_NAME } + props = { + onClose: vi.fn(), + runId: RUN_ID, + robotName: ROBOT_NAME, + runStatus: RUN_STATUS_RUNNING, + } }) afterEach(() => { @@ -101,13 +100,11 @@ describe('ConfirmCancelModal', () => { expect(mockTrackProtocolRunEvent).toHaveBeenCalled() }) it('should close modal if run status becomes stop-requested', () => { - vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_STOP_REQUESTED) - render(props) + render({ ...props, runStatus: RUN_STATUS_STOP_REQUESTED }) expect(props.onClose).toHaveBeenCalled() }) it('should close modal if run status becomes stopped', () => { - vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_STOPPED) - render(props) + render({ ...props, runStatus: RUN_STATUS_STOPPED }) expect(props.onClose).toHaveBeenCalled() }) it('should call No go back button', () => { diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolAnalysisErrorModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolAnalysisErrorModal.test.tsx similarity index 90% rename from app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolAnalysisErrorModal.test.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolAnalysisErrorModal.test.tsx index e7d2be4c976..57c9e75dbd3 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolAnalysisErrorModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolAnalysisErrorModal.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, expect, vi } from 'vitest' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../../../../__testing-utils__' +import { i18n } from '../../../../../../../i18n' import { ProtocolAnalysisErrorModal } from '../ProtocolAnalysisErrorModal' const render = ( diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolDropTipModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolDropTipModal.test.tsx similarity index 77% rename from app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolDropTipModal.test.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolDropTipModal.test.tsx index ca4608b510d..0104497089d 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolDropTipModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolDropTipModal.test.tsx @@ -2,20 +2,17 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { renderHook, act, screen, fireEvent } from '@testing-library/react' -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' - +import { renderWithProviders } from '../../../../../../../__testing-utils__' +import { i18n } from '../../../../../../../i18n' +import { useHomePipettes } from '../../../../../../DropTipWizardFlows' import { useProtocolDropTipModal, ProtocolDropTipModal, } from '../ProtocolDropTipModal' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' -import { mockLeftSpecs } from '../../../../redux/pipettes/__fixtures__' -import { useHomePipettes } from '../../../DropTipWizardFlows/hooks' import type { Mock } from 'vitest' -vi.mock('../../../DropTipWizardFlows/hooks') +vi.mock('../../../../../../DropTipWizardFlows') describe('useProtocolDropTipModal', () => { let props: Parameters[0] @@ -28,15 +25,17 @@ describe('useProtocolDropTipModal', () => { isRunCurrent: true, onSkipAndHome: vi.fn(), currentRunId: 'MOCK_ID', - mount: 'left', - instrumentModelSpecs: mockLeftSpecs, - robotType: FLEX_ROBOT_TYPE, + pipetteInfo: { + pipetteId: '123', + pipetteName: 'MOCK_NAME', + mount: 'left', + }, } mockHomePipettes = vi.fn() vi.mocked(useHomePipettes).mockReturnValue({ homePipettes: mockHomePipettes, - isHomingPipettes: false, + isHoming: false, }) }) @@ -44,10 +43,12 @@ describe('useProtocolDropTipModal', () => { const { result } = renderHook(() => useProtocolDropTipModal(props)) expect(result.current).toEqual({ - showDTModal: true, - onDTModalSkip: expect.any(Function), - onDTModalRemoval: expect.any(Function), - isDisabled: false, + showModal: true, + modalProps: { + onSkip: expect.any(Function), + onBeginRemoval: expect.any(Function), + isDisabled: false, + }, }) }) @@ -56,26 +57,26 @@ describe('useProtocolDropTipModal', () => { useProtocolDropTipModal(props) ) - expect(result.current.showDTModal).toBe(true) + expect(result.current.showModal).toBe(true) props.areTipsAttached = false rerender() - expect(result.current.showDTModal).toBe(false) + expect(result.current.showModal).toBe(false) }) it('should not show modal when isRunCurrent is false', () => { props.isRunCurrent = false const { result } = renderHook(() => useProtocolDropTipModal(props)) - expect(result.current.showDTModal).toBe(false) + expect(result.current.showModal).toBe(false) }) it('should call homePipettes when onDTModalSkip is called', () => { const { result } = renderHook(() => useProtocolDropTipModal(props)) act(() => { - result.current.onDTModalSkip() + result.current.modalProps?.onSkip() }) expect(mockHomePipettes).toHaveBeenCalled() @@ -85,7 +86,7 @@ describe('useProtocolDropTipModal', () => { const { result } = renderHook(() => useProtocolDropTipModal(props)) act(() => { - result.current.onDTModalRemoval() + result.current.modalProps?.onBeginRemoval() }) expect(props.toggleDTWiz).toHaveBeenCalled() @@ -94,12 +95,12 @@ describe('useProtocolDropTipModal', () => { it('should set isDisabled to true when isHomingPipettes is true', () => { vi.mocked(useHomePipettes).mockReturnValue({ homePipettes: mockHomePipettes, - isHomingPipettes: true, + isHoming: true, }) const { result } = renderHook(() => useProtocolDropTipModal(props)) - expect(result.current.isDisabled).toBe(true) + expect(result.current.modalProps?.isDisabled).toBe(true) }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/RunFailedModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/RunFailedModal.test.tsx similarity index 84% rename from app/src/organisms/Devices/ProtocolRun/__tests__/RunFailedModal.test.tsx rename to app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/RunFailedModal.test.tsx index affc52d8d94..e3b1bb1b972 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/RunFailedModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/RunFailedModal.test.tsx @@ -1,16 +1,16 @@ import * as React from 'react' import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' -import { useDownloadRunLog } from '../../hooks' +import { renderWithProviders } from '../../../../../../../__testing-utils__' +import { i18n } from '../../../../../../../i18n' +import { useDownloadRunLog } from '../../../../../hooks' import { RunFailedModal } from '../RunFailedModal' import { RUN_STATUS_FAILED } from '@opentrons/api-client' import type { RunError } from '@opentrons/api-client' import { fireEvent, screen } from '@testing-library/react' -vi.mock('../../hooks') +vi.mock('../../../../../hooks') const RUN_ID = '1' const ROBOT_NAME = 'mockRobotName' @@ -38,9 +38,9 @@ describe('RunFailedModal - DesktopApp', () => { props = { robotName: ROBOT_NAME, runId: RUN_ID, - setShowRunFailedModal: vi.fn(), - highestPriorityError: mockError, + toggleModal: vi.fn(), runStatus: RUN_STATUS_FAILED, + runErrors: { highestPriorityError: mockError, commandErrorList: null }, } vi.mocked(useDownloadRunLog).mockReturnValue({ downloadRunLog: vi.fn(), @@ -67,13 +67,13 @@ describe('RunFailedModal - DesktopApp', () => { it('should call a mock function when clicking close button', () => { render(props) fireEvent.click(screen.getByRole('button', { name: 'Close' })) - expect(props.setShowRunFailedModal).toHaveBeenCalled() + expect(props.toggleModal).toHaveBeenCalled() }) it('should close the modal when clicking close icon', () => { render(props) fireEvent.click(screen.getByRole('button', { name: '' })) - expect(props.setShowRunFailedModal).toHaveBeenCalled() + expect(props.toggleModal).toHaveBeenCalled() }) it('should call a mock function when clicking download run log button', () => { diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/index.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/index.ts new file mode 100644 index 00000000000..7de31bc0934 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/index.ts @@ -0,0 +1,6 @@ +export * from './ConfirmCancelModal' +export * from './ProtocolDropTipModal' +export * from './ProtocolAnalysisErrorModal' +export * from './RunFailedModal' +export * from './HeaterShakerIsRunningModal' +export * from './ConfirmMissingStepsModal' diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts new file mode 100644 index 00000000000..bb8087ef13b --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts @@ -0,0 +1,132 @@ +import { useNavigate } from 'react-router-dom' + +import { + useConfirmCancelModal, + useHeaterShakerIsRunningModal, + useProtocolAnalysisErrorsModal, + useRunFailedModal, +} from './modals' +import { + useHeaterShakerConfirmationModal, + useMissingStepsModal, + useRunHeaderDropTip, +} from './hooks' +import { useErrorRecoveryFlows } from '../../../../ErrorRecoveryFlows' +import { + useProtocolDetailsForRun, + useRobot, + useRobotType, +} from '../../../hooks' +import { getFallbackRobotSerialNumber } from '../utils' +import { + ANALYTICS_PROTOCOL_PROCEED_TO_RUN, + useTrackEvent, +} from '../../../../../redux/analytics' + +import type { AttachedModule, RunStatus, Run } from '@opentrons/api-client' +import type { UseErrorRecoveryResult } from '../../../../ErrorRecoveryFlows' +import type { + UseRunHeaderDropTipResult, + UseMissingStepsModalResult, + UseHeaterShakerConfirmationModalResult, +} from './hooks' +import type { + UseAnalysisErrorsModalResult, + UseConfirmCancelModalResult, + UseHeaterShakerIsRunningModalResult, + UseRunFailedModalResult, +} from './modals' +import type { ProtocolRunHeaderProps } from '..' +import type { RunControls } from '../../../../RunTimeControl/hooks' +import type { UseRunErrorsResult } from '../hooks' + +interface UseRunHeaderModalContainerProps extends ProtocolRunHeaderProps { + attachedModules: AttachedModule[] + protocolRunControls: RunControls + runStatus: RunStatus | null + runRecord: Run | null + runErrors: UseRunErrorsResult +} + +export interface UseRunHeaderModalContainerResult { + confirmCancelModalUtils: UseConfirmCancelModalResult + runFailedModalUtils: UseRunFailedModalResult + analysisErrorModalUtils: UseAnalysisErrorsModalResult + HSRunningModalUtils: UseHeaterShakerIsRunningModalResult + HSConfirmationModalUtils: UseHeaterShakerConfirmationModalResult + missingStepsModalUtils: UseMissingStepsModalResult + dropTipUtils: UseRunHeaderDropTipResult + recoveryModalUtils: UseErrorRecoveryResult +} + +// Provides all the utilities used by the various modals that render in ProtocolRunHeader. +export function useRunHeaderModalContainer({ + runId, + robotName, + runStatus, + runRecord, + attachedModules, + missingSetupSteps, + protocolRunControls, + runErrors, +}: UseRunHeaderModalContainerProps): UseRunHeaderModalContainerResult { + const navigate = useNavigate() + + const { displayName } = useProtocolDetailsForRun(runId) + const robot = useRobot(robotName) + const robotSerialNumber = getFallbackRobotSerialNumber(robot) + const trackEvent = useTrackEvent() + const robotType = useRobotType(robotName) + + function handleProceedToRunClick(): void { + navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) + trackEvent({ + name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, + properties: { robotSerialNumber }, + }) + protocolRunControls.play() + } + + const confirmCancelModalUtils = useConfirmCancelModal() + + const runFailedModalUtils = useRunFailedModal(runErrors) + + const analysisErrorModalUtils = useProtocolAnalysisErrorsModal({ + robotName, + runId, + displayName, + }) + + const HSRunningModalUtils = useHeaterShakerIsRunningModal(attachedModules) + + const HSConfirmationModalUtils = useHeaterShakerConfirmationModal( + handleProceedToRunClick + ) + + const missingStepsModalUtils = useMissingStepsModal({ + attachedModules, + runStatus, + missingSetupSteps, + handleProceedToRunClick, + }) + + const dropTipUtils = useRunHeaderDropTip({ + runId, + runStatus, + runRecord, + robotType, + }) + + const recoveryModalUtils = useErrorRecoveryFlows(runId, runStatus) + + return { + confirmCancelModalUtils, + analysisErrorModalUtils, + HSConfirmationModalUtils, + HSRunningModalUtils, + runFailedModalUtils, + recoveryModalUtils, + missingStepsModalUtils, + dropTipUtils, + } +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderProtocolName.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderProtocolName.tsx new file mode 100644 index 00000000000..7225ce280e0 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderProtocolName.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' +import { Link } from 'react-router-dom' + +import { + COLORS, + Flex, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { useProtocolDetailsForRun } from '../../hooks' + +interface RunHeaderProtocolNameProps { + runId: string +} + +// Styles the protocol name copy. +export function RunHeaderProtocolName({ + runId, +}: RunHeaderProtocolNameProps): JSX.Element { + const { protocolKey, displayName } = useProtocolDetailsForRun(runId) + + return ( + + {protocolKey != null ? ( + + + {displayName} + + + ) : ( + + {displayName} + + )} + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx new file mode 100644 index 00000000000..bd0bfe7e180 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx @@ -0,0 +1,183 @@ +import * as React from 'react' +import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' +import { screen } from '@testing-library/react' +import { useNavigate } from 'react-router-dom' + +import { RUN_STATUS_RUNNING } from '@opentrons/api-client' +import { useModulesQuery } from '@opentrons/react-api-client' + +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { ProtocolRunHeader } from '..' +import { useRunStatus } from '../../../../RunTimeControl/hooks' +import { useIsRobotViewable, useProtocolDetailsForRun } from '../../../hooks' +import { useNotifyRunQuery } from '../../../../../resources/runs' +import { RunHeaderModalContainer } from '../RunHeaderModalContainer' +import { RunHeaderBannerContainer } from '../RunHeaderBannerContainer' +import { RunHeaderContent } from '../RunHeaderContent' +import { RunProgressMeter } from '../../../../RunProgressMeter' +import { RunHeaderProtocolName } from '../RunHeaderProtocolName' +import { + useRunAnalytics, + useRunErrors, + useRunHeaderRunControls, +} from '../hooks' + +vi.mock('react-router-dom') +vi.mock('@opentrons/react-api-client') +vi.mock('../../../../RunTimeControl/hooks') +vi.mock('../../../hooks') +vi.mock('../../../../../resources/runs') +vi.mock('../RunHeaderModalContainer') +vi.mock('../RunHeaderBannerContainer') +vi.mock('../RunHeaderContent') +vi.mock('../../../../RunProgressMeter') +vi.mock('../RunHeaderProtocolName') +vi.mock('../hooks') + +const MOCK_PROTOCOL = 'MOCK_PROTOCOL' +const MOCK_RUN_ID = 'MOCK_RUN_ID' +const MOCK_ROBOT = 'MOCK_ROBOT' + +describe('ProtocolRunHeader', () => { + let props: React.ComponentProps + const mockNavigate = vi.fn() + + beforeEach(() => { + props = { + protocolRunHeaderRef: null, + robotName: MOCK_ROBOT, + runId: MOCK_RUN_ID, + makeHandleJumpToStep: vi.fn(), + missingSetupSteps: [], + } + + vi.mocked(useNavigate).mockReturnValue(mockNavigate) + vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_RUNNING) + vi.mocked(useIsRobotViewable).mockReturnValue(true) + vi.mocked(useProtocolDetailsForRun).mockReturnValue({ + protocolData: {} as any, + displayName: MOCK_PROTOCOL, + } as any) + vi.mocked(useNotifyRunQuery).mockReturnValue({ + data: { data: { hasEverEnteredErrorRecovery: false } }, + } as any) + vi.mocked(useModulesQuery).mockReturnValue({ + data: { data: [] }, + } as any) + vi.mocked(useRunAnalytics).mockImplementation(() => {}) + vi.mocked(useRunErrors).mockReturnValue([] as any) + vi.mocked(useRunHeaderRunControls).mockReturnValue({} as any) + + vi.mocked(RunHeaderModalContainer).mockReturnValue( +
MOCK_RUN_HEADER_MODAL_CONTAINER
+ ) + vi.mocked(RunHeaderBannerContainer).mockReturnValue( +
MOCK_RUN_HEADER_BANNER_CONTAINER
+ ) + vi.mocked(RunHeaderContent).mockReturnValue( +
MOCK_RUN_HEADER_CONTENT
+ ) + vi.mocked(RunProgressMeter).mockReturnValue( +
MOCK_RUN_PROGRESS_METER
+ ) + vi.mocked(RunHeaderProtocolName).mockReturnValue( +
MOCK_RUN_HEADER_PROTOCOL_NAME
+ ) + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] + } + + it('renders all components', () => { + render(props) + + screen.getByText('MOCK_RUN_HEADER_MODAL_CONTAINER') + screen.getByText('MOCK_RUN_HEADER_PROTOCOL_NAME') + screen.getByText('MOCK_RUN_HEADER_BANNER_CONTAINER') + screen.getByText('MOCK_RUN_HEADER_CONTENT') + screen.getByText('MOCK_RUN_PROGRESS_METER') + }) + + it('navigates to /devices if robot is not viewable and protocolData is not null', () => { + vi.mocked(useIsRobotViewable).mockReturnValue(false) + vi.mocked(useProtocolDetailsForRun).mockReturnValue({ + protocolData: {} as any, + displayName: MOCK_PROTOCOL, + } as any) + + render(props) + + expect(mockNavigate).toHaveBeenCalledWith('/devices') + }) + + it('does not navigate if protocolData is null', () => { + vi.mocked(useIsRobotViewable).mockReturnValue(false) + vi.mocked(useProtocolDetailsForRun).mockReturnValue({ + protocolData: null, + displayName: MOCK_PROTOCOL, + } as any) + + render(props) + + expect(mockNavigate).not.toHaveBeenCalledWith('/devices') + }) + + it('calls useRunAnalytics with correct parameters', () => { + render(props) + + expect(useRunAnalytics).toHaveBeenCalledWith({ + runId: MOCK_RUN_ID, + robotName: MOCK_ROBOT, + enteredER: false, + }) + }) + + it('passes correct props to RunHeaderModalContainer', () => { + render(props) + + expect(RunHeaderModalContainer).toHaveBeenCalledWith( + expect.objectContaining({ + runStatus: RUN_STATUS_RUNNING, + runErrors: [], + protocolRunControls: expect.any(Object), + }), + expect.anything() + ) + }) + + it('passes correct props to RunHeaderBannerContainer', () => { + render(props) + + expect(RunHeaderBannerContainer).toHaveBeenCalledWith( + expect.objectContaining({ + runStatus: RUN_STATUS_RUNNING, + enteredER: false, + isResetRunLoading: false, + runErrors: [], + }), + expect.anything() + ) + }) + + it('passes correct props to RunHeaderContent', () => { + render(props) + + expect(RunHeaderContent).toHaveBeenCalledWith( + expect.objectContaining({ + runStatus: RUN_STATUS_RUNNING, + isResetRunLoadingRef: expect.any(Object), + attachedModules: [], + protocolRunControls: expect.any(Object), + }), + expect.anything() + ) + }) +}) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/constants.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/constants.ts new file mode 100644 index 00000000000..604999b6355 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/constants.ts @@ -0,0 +1 @@ +export const EQUIPMENT_POLL_MS = 5000 diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/index.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/index.ts new file mode 100644 index 00000000000..fa59698a7af --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/index.ts @@ -0,0 +1,4 @@ +export * from './useIsDoorOpen' +export * from './useRunAnalytics' +export * from './useRunHeaderRunControls' +export * from './useRunErrors' diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useIsDoorOpen.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useIsDoorOpen.ts new file mode 100644 index 00000000000..c16f37e3d21 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useIsDoorOpen.ts @@ -0,0 +1,36 @@ +import { useSelector } from 'react-redux' + +import { useDoorQuery } from '@opentrons/react-api-client' + +import { getRobotSettings } from '../../../../../redux/robot-settings' +import { useIsFlex } from '../../../hooks' +import { EQUIPMENT_POLL_MS } from '../constants' + +import type { State } from '../../../../../redux/types' + +export function useIsDoorOpen(robotName: string): boolean { + const robotSettings = useSelector((state: State) => + getRobotSettings(state, robotName) + ) + const isFlex = useIsFlex(robotName) + + const doorSafetySetting = robotSettings.find( + setting => setting.id === 'enableDoorSafetySwitch' + ) + + const { data: doorStatus } = useDoorQuery({ + refetchInterval: EQUIPMENT_POLL_MS, + }) + + let isDoorOpen: boolean + const isStatusOpen = doorStatus?.data.status === 'open' + const isDoorSafetyEnabled = Boolean(doorSafetySetting?.value) + + if (isFlex || (!isFlex && isDoorSafetyEnabled)) { + isDoorOpen = isStatusOpen + } else { + isDoorOpen = false + } + + return isDoorOpen +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts new file mode 100644 index 00000000000..3cca75c6eb9 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts @@ -0,0 +1,48 @@ +import * as React from 'react' +import { useRobotAnalyticsData, useTrackProtocolRunEvent } from '../../../hooks' +import { useRunStatus } from '../../../../RunTimeControl/hooks' +import { useIsRunCurrent } from '../../../../../resources/runs' +import { ANALYTICS_PROTOCOL_RUN_ACTION } from '../../../../../redux/analytics' + +import { useRecoveryAnalytics } from '../../../../ErrorRecoveryFlows/hooks' +import { isTerminalRunStatus } from '../utils' + +interface UseRunAnalyticsProps { + runId: string | null + robotName: string + enteredER: boolean +} + +// Implicitly send reports related to the run when the current run is terminal. +export function useRunAnalytics({ + runId, + robotName, + enteredER, +}: UseRunAnalyticsProps): void { + const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) + const robotAnalyticsData = useRobotAnalyticsData(robotName) + const runStatus = useRunStatus(runId) + const isRunCurrent = useIsRunCurrent(runId) + + React.useEffect(() => { + const areReportConditionsValid = + isRunCurrent && + runId != null && + robotAnalyticsData != null && + isTerminalRunStatus(runStatus) + + if (areReportConditionsValid) { + trackProtocolRunEvent({ + name: ANALYTICS_PROTOCOL_RUN_ACTION.FINISH, + properties: robotAnalyticsData, + }) + } + }, [runStatus, isRunCurrent, runId, robotAnalyticsData]) + + const { reportRecoveredRunResult } = useRecoveryAnalytics() + React.useEffect(() => { + if (isRunCurrent) { + reportRecoveredRunResult(runStatus, enteredER) + } + }, [isRunCurrent, enteredER]) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunErrors.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunErrors.ts new file mode 100644 index 00000000000..58d39855f44 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunErrors.ts @@ -0,0 +1,50 @@ +import { useRunCommandErrors } from '@opentrons/react-api-client' + +import { isTerminalRunStatus } from '../utils' +import { useMostRecentRunId } from '../../../../ProtocolUpload/hooks/useMostRecentRunId' +import { getHighestPriorityError } from '../../../../ODD/RunningProtocol' + +import type { RunStatus, Run } from '@opentrons/api-client' +import type { RunCommandError } from '@opentrons/shared-data' + +// A reasonably high number of commands that a user would realistically care to examine. +const ALL_COMMANDS_PAGE_LENGTH = 100 + +interface UseRunErrorsProps { + runId: string + runStatus: RunStatus | null + runRecord: Run | null +} + +export interface UseRunErrorsResult { + commandErrorList: RunCommandError[] | null + highestPriorityError: RunCommandError | null +} + +// During a run, a single error or multiple errors may occur, and currently, these are managed under separate endpoints. +export function useRunErrors({ + runId, + runRecord, + runStatus, +}: UseRunErrorsProps): UseRunErrorsResult { + const mostRecentRunId = useMostRecentRunId() + const isMostRecentRun = mostRecentRunId === runId + + const { data: commandErrorList } = useRunCommandErrors( + runId, + { cursor: 0, pageLength: ALL_COMMANDS_PAGE_LENGTH }, + { + enabled: isTerminalRunStatus(runStatus) && isMostRecentRun, + } + ) + + const highestPriorityError = + runRecord?.data.errors?.[0] != null + ? getHighestPriorityError(runRecord.data.errors as RunCommandError[]) + : null + + return { + commandErrorList: commandErrorList?.data ?? null, + highestPriorityError, + } +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunHeaderRunControls.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunHeaderRunControls.ts new file mode 100644 index 00000000000..dd8bdd916c3 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunHeaderRunControls.ts @@ -0,0 +1,20 @@ +import { useNavigate } from 'react-router-dom' +import { useRunControls } from '../../../../RunTimeControl/hooks' + +import type { Run } from '@opentrons/api-client' +import type { RunControls } from '../../../../RunTimeControl/hooks' + +// Provides desktop run controls, routing the user to the run preview tab after a "run again" action. +export function useRunHeaderRunControls( + runId: string, + robotName: string +): RunControls { + const navigate = useNavigate() + + function handleRunReset(createRunResponse: Run): void { + navigate( + `/devices/${robotName}/protocol-runs/${createRunResponse.data.id}/run-preview` + ) + } + return useRunControls(runId, handleRunReset) +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/index.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/index.tsx new file mode 100644 index 00000000000..3b9711e3e60 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/index.tsx @@ -0,0 +1,126 @@ +import * as React from 'react' +import { useNavigate } from 'react-router-dom' +import { css } from 'styled-components' + +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + SPACING, +} from '@opentrons/components' +import { useModulesQuery } from '@opentrons/react-api-client' +import { RUN_STATUS_IDLE, RUN_STATUS_RUNNING } from '@opentrons/api-client' + +import { useRunStatus } from '../../../RunTimeControl/hooks' +import { useIsRobotViewable, useProtocolDetailsForRun } from '../../hooks' +import { RunProgressMeter } from '../../../RunProgressMeter' +import { useNotifyRunQuery } from '../../../../resources/runs' +import { RunHeaderProtocolName } from './RunHeaderProtocolName' +import { + RunHeaderModalContainer, + useRunHeaderModalContainer, +} from './RunHeaderModalContainer' +import { RunHeaderBannerContainer } from './RunHeaderBannerContainer' +import { useRunAnalytics, useRunErrors, useRunHeaderRunControls } from './hooks' +import { RunHeaderContent } from './RunHeaderContent' +import { EQUIPMENT_POLL_MS } from './constants' +import { isCancellableStatus } from './utils' + +export interface ProtocolRunHeaderProps { + protocolRunHeaderRef: React.RefObject | null + robotName: string + runId: string + makeHandleJumpToStep: (index: number) => () => void + missingSetupSteps: string[] +} + +export function ProtocolRunHeader( + props: ProtocolRunHeaderProps +): JSX.Element | null { + const { protocolRunHeaderRef, robotName, runId } = props + + const navigate = useNavigate() + + const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) + const { protocolData } = useProtocolDetailsForRun(runId) + const isRobotViewable = useIsRobotViewable(robotName) + const runStatus = useRunStatus(runId) + const attachedModules = + useModulesQuery({ + refetchInterval: EQUIPMENT_POLL_MS, + enabled: isCancellableStatus(runStatus), + })?.data?.data ?? [] + const runErrors = useRunErrors({ + runRecord: runRecord ?? null, + runStatus, + runId, + }) + + const enteredER = runRecord?.data.hasEverEnteredErrorRecovery ?? false + const protocolRunControls = useRunHeaderRunControls(runId, robotName) + const runHeaderModalContainerUtils = useRunHeaderModalContainer({ + ...props, + attachedModules, + runStatus, + protocolRunControls, + runRecord: runRecord ?? null, + runErrors, + }) + + React.useEffect(() => { + if (protocolData != null && !isRobotViewable) { + navigate('/devices') + } + }, [protocolData, isRobotViewable, navigate]) + + // To persist "run again" loading conditions into a new run, we need a scalar that persists longer than + // the runControl isResetRunLoading, which completes before we want to change user-facing copy/CTAs. + const isResetRunLoadingRef = React.useRef(false) + if (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_RUNNING) { + isResetRunLoadingRef.current = false + } + + useRunAnalytics({ runId, robotName, enteredER }) + + return ( + <> + + + + + + + + + ) +} + +const CONTAINER_STYLE = css` + background-color: ${COLORS.white}; + border-radius: ${BORDERS.borderRadius8}; + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing16}; + margin-bottom: ${SPACING.spacing16}; + padding: ${SPACING.spacing16}; +` diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/utils.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/utils.ts new file mode 100644 index 00000000000..a2746494374 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/utils.ts @@ -0,0 +1,87 @@ +import { + RUN_STATUS_IDLE, + RUN_STATUS_PAUSED, + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_FAILED, + RUN_STATUS_STOPPED, + RUN_STATUS_FINISHING, + RUN_STATUS_SUCCEEDED, + RUN_STATUS_RUNNING, + RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, + RUN_STATUS_STOP_REQUESTED, +} from '@opentrons/api-client' + +import { getRobotSerialNumber } from '../../../../redux/discovery' + +import type { RunStatus } from '@opentrons/api-client' +import type { DiscoveredRobot } from '../../../../redux/discovery/types' + +const START_RUN_STATUSES: RunStatus[] = [ + RUN_STATUS_IDLE, + RUN_STATUS_PAUSED, + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, +] +const RUN_AGAIN_STATUSES: RunStatus[] = [ + RUN_STATUS_STOPPED, + RUN_STATUS_FINISHING, + RUN_STATUS_FAILED, + RUN_STATUS_SUCCEEDED, +] +const RECOVERY_STATUSES: RunStatus[] = [ + RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, +] +const DISABLED_STATUSES: RunStatus[] = [ + RUN_STATUS_FINISHING, + RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + ...RECOVERY_STATUSES, +] +const CANCELLABLE_STATUSES: RunStatus[] = [ + RUN_STATUS_RUNNING, + RUN_STATUS_PAUSED, + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_IDLE, + RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, +] +const TERMINAL_STATUSES: RunStatus[] = [ + RUN_STATUS_STOPPED, + RUN_STATUS_SUCCEEDED, + RUN_STATUS_FAILED, +] + +export function isTerminalRunStatus(runStatus: RunStatus | null): boolean { + return runStatus !== null && TERMINAL_STATUSES.includes(runStatus) +} + +export function isStartRunStatus(runStatus: RunStatus | null): boolean { + return runStatus !== null && START_RUN_STATUSES.includes(runStatus) +} + +export function isRunAgainStatus(runStatus: RunStatus | null): boolean { + return runStatus !== null && RUN_AGAIN_STATUSES.includes(runStatus) +} + +export function isRecoveryStatus(runStatus: RunStatus | null): boolean { + return runStatus !== null && RECOVERY_STATUSES.includes(runStatus) +} + +export function isDisabledStatus(runStatus: RunStatus | null): boolean { + return runStatus !== null && DISABLED_STATUSES.includes(runStatus) +} + +export function isCancellableStatus(runStatus: RunStatus | null): boolean { + return runStatus !== null && CANCELLABLE_STATUSES.includes(runStatus) +} + +export function getFallbackRobotSerialNumber( + robot: DiscoveredRobot | null +): string { + const sn = robot?.status != null ? getRobotSerialNumber(robot) : null + return sn ?? '' +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx index 4930efee2d3..1ed25b251c4 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useInstrumentsQuery } from '@opentrons/react-api-client' +import { useTranslation } from 'react-i18next' import { COLORS, DIRECTION_COLUMN, @@ -74,6 +75,7 @@ export const ProtocolRunModuleControls = ({ robotName, runId, }: ProtocolRunModuleControlsProps): JSX.Element => { + const { t } = useTranslation('protocol_setup') const { attachPipetteRequired, calibratePipetteRequired, @@ -102,7 +104,7 @@ export const ProtocolRunModuleControls = ({ padding={SPACING.spacing16} backgroundColor={COLORS.white} > - +
) : ( diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index b1d52c03763..80373199cca 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -108,32 +108,17 @@ export function ProtocolRunRuntimeParameters({ ) : null} {hasRunTimeParameters ? ( - - - - {t('values_are_view_only')} - - - {t('cancel_and_restart_to_edit')} - - - + ) : null}
{!hasRunTimeParameters ? ( ) : ( @@ -171,6 +156,31 @@ export function ProtocolRunRuntimeParameters({ ) } +interface RunTimeParametersBannerProps { + isRunTerminal: boolean +} + +function RunTimeParametersBanner({ + isRunTerminal, +}: RunTimeParametersBannerProps): JSX.Element { + const { t } = useTranslation('protocol_setup') + + return ( + + + + {isRunTerminal ? t('download_files') : t('values_are_view_only')} + + + {isRunTerminal + ? t('all_files_associated') + : t('cancel_and_restart_to_edit')} + + + + ) +} + interface StyledTableRowComponentProps { parameter: RunTimeParameter index: number @@ -218,8 +228,12 @@ const StyledTableRowComponent = ( ) : null} - - + + {parameter.type === 'csv_file' ? parameter.file?.name ?? '' : formatRunTimeParameterValue(parameter, t)} @@ -279,3 +293,12 @@ const StyledTableCell = styled.td` padding-right: ${props => props.paddingRight != null ? props.paddingRight : SPACING.spacing16}; ` + +const PARAMETER_VALUE_TEXT_STYLE = css` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + overflow-wrap: anywhere; +` diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx index 604bd253cd0..581fd16fec1 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -18,7 +18,6 @@ import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE, parseAllRequiredModuleModels, - parseLiquidsInLoadOrder, } from '@opentrons/shared-data' import { Line } from '../../../atoms/structure' @@ -165,11 +164,7 @@ export function ProtocolRunSetup({ }) const liquids = protocolAnalysis?.liquids ?? [] - const liquidsInLoadOrder = - protocolAnalysis != null - ? parseLiquidsInLoadOrder(liquids, protocolAnalysis.commands) - : [] - const hasLiquids = liquidsInLoadOrder.length > 0 + const hasLiquids = liquids.length > 0 const hasModules = protocolAnalysis != null && modules.length > 0 // need config compatibility (including check for single slot conflicts) const requiredDeckConfigCompatibility = getRequiredDeckConfig( @@ -191,11 +186,17 @@ export function ProtocolRunSetup({ const [liquidSetupComplete, setLiquidSetupComplete] = React.useState( !hasLiquids ) - if (!hasLiquids && missingSteps.includes('liquids')) { + if ( + !hasLiquids && + protocolAnalysis != null && + missingSteps.includes('liquids') + ) { setMissingSteps(missingSteps.filter(step => step !== 'liquids')) } const [lpcComplete, setLpcComplete] = React.useState(false) - if (robot == null) return null + if (robot == null) { + return null + } const StepDetailMap: Record< StepKey, { @@ -252,7 +253,6 @@ export function ProtocolRunSetup({ rightElProps: { stepKey: MODULE_SETUP_KEY, complete: - calibrationStatusRobot.complete && calibrationStatusModules.complete && !isMissingModule && !isFixtureMismatch, diff --git a/app/src/organisms/Devices/ProtocolRun/SetupFlexPipetteCalibrationItem.tsx b/app/src/organisms/Devices/ProtocolRun/SetupFlexPipetteCalibrationItem.tsx index 49ed6243cf8..c7849e4f489 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupFlexPipetteCalibrationItem.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupFlexPipetteCalibrationItem.tsx @@ -16,6 +16,7 @@ import { import { useInstrumentsQuery } from '@opentrons/react-api-client' import { TertiaryButton } from '../../../atoms/buttons' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useStoredProtocolAnalysis } from '../hooks' import { PipetteWizardFlows } from '../../PipetteWizardFlows' import { FLOWS } from '../../PipetteWizardFlows/constants' import { SetupCalibrationItem } from './SetupCalibrationItem' @@ -40,11 +41,13 @@ export function SetupFlexPipetteCalibrationItem({ ) const { data: attachedInstruments } = useInstrumentsQuery() const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const loadPipetteCommand = mostRecentAnalysis?.commands.find( + const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) + const completedAnalysis = mostRecentAnalysis ?? storedProtocolAnalysis + const loadPipetteCommand = completedAnalysis?.commands.find( (command): command is LoadPipetteRunTimeCommand => command.commandType === 'loadPipette' && command.params.mount === mount ) - const requestedPipette = mostRecentAnalysis?.pipettes?.find( + const requestedPipette = completedAnalysis?.pipettes?.find( pipette => pipette.id === loadPipetteCommand?.result?.pipetteId ) @@ -120,7 +123,7 @@ export function SetupFlexPipetteCalibrationItem({ ? NINETY_SIX_CHANNEL : SINGLE_MOUNT_PIPETTES } - pipetteInfo={mostRecentAnalysis?.pipettes} + pipetteInfo={completedAnalysis?.pipettes} onComplete={instrumentsRefetch} /> )} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx index 1c9f05aaa47..a42596b58e6 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx @@ -50,7 +50,7 @@ import type { LoadLabwareRunTimeCommand, } from '@opentrons/shared-data' import type { ModuleRenderInfoForProtocol } from '../../hooks' -import type { LabwareSetupItem } from '../../../../pages/Protocols/utils' +import type { LabwareSetupItem } from '../../../../transformations/commands' import type { ModuleTypesThatRequireExtraAttention } from '../utils/getModuleTypesThatRequireExtraAttention' import type { NestedLabwareInfo } from './getNestedLabwareInfo' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/OffDeckLabwareList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/OffDeckLabwareList.tsx index 121f4588691..848f0d3247c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/OffDeckLabwareList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/OffDeckLabwareList.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { SPACING, TYPOGRAPHY, LegacyStyledText } from '@opentrons/components' import { LabwareListItem } from './LabwareListItem' import type { RunTimeCommand } from '@opentrons/shared-data' -import type { LabwareSetupItem } from '../../../../pages/Protocols/utils' +import type { LabwareSetupItem } from '../../../../transformations/commands' interface OffDeckLabwareListProps { labwareItems: LabwareSetupItem[] diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx index a86da22ace8..229dd98281c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx @@ -7,14 +7,14 @@ import { StyledText, COLORS, } from '@opentrons/components' -import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils' +import { getLabwareSetupItemGroups } from '../../../../transformations/commands' import { LabwareListItem } from './LabwareListItem' import { getNestedLabwareInfo } from './getNestedLabwareInfo' import type { RunTimeCommand } from '@opentrons/shared-data' import type { ModuleRenderInfoForProtocol } from '../../hooks' import type { ModuleTypesThatRequireExtraAttention } from '../utils/getModuleTypesThatRequireExtraAttention' -import type { LabwareSetupItem } from '../../../../pages/Protocols/utils' +import type { LabwareSetupItem } from '../../../../transformations/commands' interface SetupLabwareListProps { attachedModuleInfo: { [moduleId: string]: ModuleRenderInfoForProtocol } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index c95adc49b36..cd611397850 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -16,7 +16,7 @@ import { THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' -import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils' +import { getLabwareSetupItemGroups } from '../../../../transformations/commands' import { LabwareInfoOverlay } from '../LabwareInfoOverlay' import { getLabwareRenderInfo } from '../utils/getLabwareRenderInfo' import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo' @@ -88,6 +88,10 @@ export function SetupLabwareMap({ topLabwareDefinition != null && topLabwareId != null && hoverLabwareId === topLabwareId, + highlightShadowLabware: + topLabwareDefinition != null && + topLabwareId != null && + hoverLabwareId === topLabwareId, stacked: topLabwareDefinition != null && topLabwareId != null, moduleChildren: ( // open modal @@ -148,6 +152,7 @@ export function SetupLabwareMap({ topLabwareId, topLabwareDisplayName, highlight: isLabwareInStack && hoverLabwareId === topLabwareId, + highlightShadow: isLabwareInStack && hoverLabwareId === topLabwareId, labwareChildren: ( { } }) -vi.mock('../../../../ProtocolSetupModulesAndDeck/utils') +vi.mock('../../../../ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils') vi.mock('../../LabwareInfoOverlay') vi.mock('../../utils/getLabwareRenderInfo') vi.mock('../../utils/getModuleTypesThatRequireExtraAttention') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/getNestedLabwareInfo.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/getNestedLabwareInfo.test.tsx index 24c50ca3efa..737a5baf1db 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/getNestedLabwareInfo.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/getNestedLabwareInfo.test.tsx @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest' import { mockDefinition } from '../../../../../redux/custom-labware/__fixtures__' import { getNestedLabwareInfo } from '../getNestedLabwareInfo' import type { RunTimeCommand } from '@opentrons/shared-data' -import type { LabwareSetupItem } from '../../../../../pages/Protocols/utils' +import type { LabwareSetupItem } from '../../../../../transformations/commands' const MOCK_LABWARE_ID = 'mockLabwareId' const MOCK_OTHER_LABWARE_ID = 'mockOtherLabwareId' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/getNestedLabwareInfo.ts b/app/src/organisms/Devices/ProtocolRun/SetupLabware/getNestedLabwareInfo.ts index 46f9d643123..c24e2b0aa3f 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/getNestedLabwareInfo.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/getNestedLabwareInfo.ts @@ -4,7 +4,7 @@ import type { LoadModuleRunTimeCommand, RunTimeCommand, } from '@opentrons/shared-data' -import type { LabwareSetupItem } from '../../../../pages/Protocols/utils' +import type { LabwareSetupItem } from '../../../../transformations/commands' export interface NestedLabwareInfo { nestedLabwareDisplayName: string diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx index 098902f046f..b1260de8c83 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx @@ -25,9 +25,9 @@ import { useAttachedModules } from '../../../hooks' import { LabwareInfoOverlay } from '../../LabwareInfoOverlay' import { getLabwareRenderInfo } from '../../utils/getLabwareRenderInfo' import { getStandardDeckViewLayerBlockList } from '../../utils/getStandardDeckViewLayerBlockList' -import { getAttachedProtocolModuleMatches } from '../../../../ProtocolSetupModulesAndDeck/utils' +import { getAttachedProtocolModuleMatches } from '../../../../ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../../utils/getProtocolModulesInfo' -import { mockProtocolModuleInfo } from '../../../../ProtocolSetupLabware/__fixtures__' +import { mockProtocolModuleInfo } from '../../../../ODD/ProtocolSetup/ProtocolSetupLabware/__fixtures__' import { mockFetchModulesSuccessActionPayloadModules } from '../../../../../redux/modules/__fixtures__' import { SetupLiquidsMap } from '../SetupLiquidsMap' @@ -52,7 +52,7 @@ vi.mock('../../LabwareInfoOverlay') vi.mock('../../../hooks') vi.mock('../utils') vi.mock('../../utils/getLabwareRenderInfo') -vi.mock('../../../../ProtocolSetupModulesAndDeck/utils') +vi.mock('../../../../ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils') vi.mock('../../utils/getProtocolModulesInfo') vi.mock('../../../../../resources/deck_configuration/utils') vi.mock('@opentrons/shared-data', async importOriginal => { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx index 79c1a0a4a3f..901c01c13bc 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx @@ -27,6 +27,9 @@ import { THERMOCYCLER_MODULE_V2, getCutoutFixturesForModuleModel, getFixtureIdByCutoutIdFromModuleSlotName, + SINGLE_LEFT_SLOT_FIXTURE, + THERMOCYCLER_V2_FRONT_FIXTURE, + THERMOCYCLER_V2_REAR_FIXTURE, } from '@opentrons/shared-data' import { getTopPortalEl } from '../../../../App/portal' @@ -76,25 +79,20 @@ export const LocationConflictModal = ( (deckFixture: CutoutConfig) => deckFixture.cutoutId === cutoutId )?.cutoutFixtureId - const isThermocycler = + const isThermocyclerRequired = requiredModule === THERMOCYCLER_MODULE_V1 || requiredModule === THERMOCYCLER_MODULE_V2 + // check if current fixture in cutoutId is thermocycler + const isThermocyclerCurrentFixture = + deckConfigurationAtLocationFixtureId === THERMOCYCLER_V2_REAR_FIXTURE || + deckConfigurationAtLocationFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE + const currentFixtureDisplayName = deckConfigurationAtLocationFixtureId != null ? getFixtureDisplayName(deckConfigurationAtLocationFixtureId) : '' - // get fixture display name at A1 for themocycler if B1 is slot - const deckConfigurationAtA1 = deckConfig.find( - (deckFixture: CutoutConfig) => deckFixture.cutoutId === 'cutoutA1' - )?.cutoutFixtureId - - const currentThermocyclerFixtureDisplayName = - currentFixtureDisplayName === 'Slot' && deckConfigurationAtA1 != null - ? getFixtureDisplayName(deckConfigurationAtA1) - : currentFixtureDisplayName - const handleConfigureModule = (moduleSerialNumber?: string): void => { if (requiredModule != null) { const slotName = cutoutId.replace('cutout', '') @@ -111,14 +109,35 @@ export const LocationConflictModal = ( const newDeckConfig = deckConfig.map(existingCutoutConfig => { const replacementCutoutFixtureId = moduleFixtureIdByCutoutId[existingCutoutConfig.cutoutId] - return existingCutoutConfig.cutoutId in moduleFixtureIdByCutoutId && + if ( + existingCutoutConfig.cutoutId in moduleFixtureIdByCutoutId && replacementCutoutFixtureId != null - ? { - ...existingCutoutConfig, - cutoutFixtureId: replacementCutoutFixtureId, - opentronsModuleSerialNumber: moduleSerialNumber, - } - : existingCutoutConfig + ) { + return { + ...existingCutoutConfig, + cutoutFixtureId: replacementCutoutFixtureId, + opentronsModuleSerialNumber: moduleSerialNumber, + } + } else if ( + isThermocyclerCurrentFixture && + ((cutoutId === 'cutoutA1' && + existingCutoutConfig.cutoutId === 'cutoutB1') || + (cutoutId === 'cutoutB1' && + existingCutoutConfig.cutoutId === 'cutoutA1')) + ) { + /** + * special-case for removing current thermocycler: + * set paired cutout (B1 for A1, A1 for B1) to single slot left fixture + * TODO(bh, 2024-08-29): generalize to remove all entities from FixtureGroup + */ + return { + ...existingCutoutConfig, + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + opentronsModuleSerialNumber: undefined, + } + } else { + return existingCutoutConfig + } }) updateDeckConfiguration(newDeckConfig) } @@ -129,15 +148,33 @@ export const LocationConflictModal = ( if (requiredModule != null) { setShowModuleSelect(true) } else if (requiredFixtureId != null) { - const newRequiredFixtureDeckConfig = deckConfig.map(fixture => - fixture.cutoutId === cutoutId - ? { - ...fixture, - cutoutFixtureId: requiredFixtureId, - opentronsModuleSerialNumber: undefined, - } - : fixture - ) + const newRequiredFixtureDeckConfig = deckConfig.map(fixture => { + if (fixture.cutoutId === cutoutId) { + return { + ...fixture, + cutoutFixtureId: requiredFixtureId, + opentronsModuleSerialNumber: undefined, + } + } else if ( + isThermocyclerCurrentFixture && + ((cutoutId === 'cutoutA1' && fixture.cutoutId === 'cutoutB1') || + (cutoutId === 'cutoutB1' && fixture.cutoutId === 'cutoutA1')) + ) { + /** + * special-case for removing current thermocycler: + * set paired cutout (B1 for A1, A1 for B1) to single slot left fixture + * TODO(bh, 2024-08-29): generalize to remove all entities from FixtureGroup + */ + return { + ...fixture, + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + opentronsModuleSerialNumber: undefined, + } + } else { + return fixture + } + }) + updateDeckConfiguration(newRequiredFixtureDeckConfig) onCloseClick() } else { @@ -154,7 +191,7 @@ export const LocationConflictModal = ( protocolSpecifiesDisplayName = getModuleDisplayName(requiredModule) } - const displaySlotName = isThermocycler + const displaySlotName = isThermocyclerRequired ? 'A1 + B1' : getCutoutDisplayName(cutoutId) @@ -189,7 +226,7 @@ export const LocationConflictModal = ( - {isThermocycler - ? currentThermocyclerFixtureDisplayName - : currentFixtureDisplayName} + {currentFixtureDisplayName} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx index 72687bc8324..272f19c2b3c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx @@ -14,7 +14,7 @@ import { } from '@opentrons/shared-data' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' +import { getAttachedProtocolModuleMatches } from '../../../ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils' import { ModuleInfo } from '../../ModuleInfo' import { useAttachedModules, useStoredProtocolAnalysis } from '../../hooks' import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx index e08ff19f415..b6c4984e6c2 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx @@ -15,7 +15,7 @@ import { mockMagneticModule as mockMagneticModuleFixture, } from '../../../../../redux/modules/__fixtures__/index' import { useMostRecentCompletedAnalysis } from '../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { getAttachedProtocolModuleMatches } from '../../../../ProtocolSetupModulesAndDeck/utils' +import { getAttachedProtocolModuleMatches } from '../../../../ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils' import { ModuleInfo } from '../../../ModuleInfo' import { SetupModulesMap } from '../SetupModulesMap' @@ -44,7 +44,7 @@ vi.mock('@opentrons/shared-data', async importOriginal => { } }) vi.mock('../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') -vi.mock('../../../../ProtocolSetupModulesAndDeck/utils') +vi.mock('../../../../ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils') vi.mock('../../../ModuleInfo') vi.mock('../../../hooks') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupTipLengthCalibrationButton.tsx b/app/src/organisms/Devices/ProtocolRun/SetupTipLengthCalibrationButton.tsx index c9d5575965f..dd9d56ef135 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupTipLengthCalibrationButton.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupTipLengthCalibrationButton.tsx @@ -26,7 +26,7 @@ import { useDeckCalibrationData, useRunHasStarted, } from '../hooks' -import { useDashboardCalibrateTipLength } from '../../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' +import { useDashboardCalibrateTipLength } from '../../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' import type { Mount } from '@opentrons/components' import type { LabwareDefinition2 } from '@opentrons/shared-data' diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx deleted file mode 100644 index ee91f1ff901..00000000000 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ /dev/null @@ -1,1057 +0,0 @@ -import * as React from 'react' -import { BrowserRouter } from 'react-router-dom' -import { fireEvent, screen, waitFor } from '@testing-library/react' -import { when } from 'vitest-when' -import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' - -import { - RUN_STATUS_IDLE, - RUN_STATUS_RUNNING, - RUN_STATUS_PAUSED, - RUN_STATUS_STOP_REQUESTED, - RUN_STATUS_STOPPED, - RUN_STATUS_FAILED, - RUN_STATUS_SUCCEEDED, - RUN_STATUS_BLOCKED_BY_OPEN_DOOR, - instrumentsResponseLeftPipetteFixture, -} from '@opentrons/api-client' -import { - useHost, - useModulesQuery, - usePipettesQuery, - useDismissCurrentRunMutation, - useEstopQuery, - useDoorQuery, - useInstrumentsQuery, - useRunCommandErrors, -} from '@opentrons/react-api-client' -import { - getPipetteModelSpecs, - STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, - simple_v6 as _uncastedSimpleV6Protocol, - simple_v4 as noModulesProtocol, -} from '@opentrons/shared-data' - -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' -import { useCloseCurrentRun } from '../../../../organisms/ProtocolUpload/hooks' -import { ConfirmCancelModal } from '../../../../organisms/RunDetails/ConfirmCancelModal' -import { - useRunTimestamps, - useRunControls, - useRunStatus, -} from '../../../../organisms/RunTimeControl/hooks' -import { - mockFailedRun, - mockIdleUnstartedRun, - mockPausedRun, - mockRunningRun, - mockStoppedRun, - mockStopRequestedRun, - mockSucceededRun, -} from '../../../../organisms/RunTimeControl/__fixtures__' -import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' -import { - useTrackEvent, - ANALYTICS_PROTOCOL_PROCEED_TO_RUN, - ANALYTICS_PROTOCOL_RUN_ACTION, -} from '../../../../redux/analytics' -import { mockConnectableRobot } from '../../../../redux/discovery/__fixtures__' -import { getRobotUpdateDisplayInfo } from '../../../../redux/robot-update' -import { getIsHeaterShakerAttached } from '../../../../redux/config' -import { getRobotSettings } from '../../../../redux/robot-settings' -import { - useProtocolDetailsForRun, - useProtocolAnalysisErrors, - useTrackProtocolRunEvent, - useRunCalibrationStatus, - useRunCreatedAtTimestamp, - useModuleCalibrationStatus, - useUnmatchedModulesForProtocol, - useIsRobotViewable, - useIsFlex, - useRobot, -} from '../../hooks' -import { useIsHeaterShakerInProtocol } from '../../../ModuleCard/hooks' -import { ConfirmAttachmentModal } from '../../../ModuleCard/ConfirmAttachmentModal' -import { RunProgressMeter } from '../../../RunProgressMeter' -import { formatTimestamp } from '../../utils' -import { ProtocolRunHeader } from '../ProtocolRunHeader' -import { HeaterShakerIsRunningModal } from '../../HeaterShakerIsRunningModal' -import { RunFailedModal } from '../RunFailedModal' -import { DISENGAGED, NOT_PRESENT } from '../../../EmergencyStop' -import { getIsFixtureMismatch } from '../../../../resources/deck_configuration/utils' -import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' -import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useMostRecentRunId } from '../../../ProtocolUpload/hooks/useMostRecentRunId' -import { useNotifyRunQuery, useCurrentRunId } from '../../../../resources/runs' -import { - useDropTipWizardFlows, - useTipAttachmentStatus, - DropTipWizardFlows, -} from '../../../DropTipWizardFlows' -import { - useErrorRecoveryFlows, - ErrorRecoveryFlows, -} from '../../../ErrorRecoveryFlows' -import { - ProtocolDropTipModal, - useProtocolDropTipModal, -} from '../ProtocolDropTipModal' -import { ConfirmMissingStepsModal } from '../ConfirmMissingStepsModal' - -import type { MissingSteps } from '../ProtocolRunSetup' -import type { UseQueryResult } from 'react-query' -import type { NavigateFunction } from 'react-router-dom' -import type { Mock } from 'vitest' -import type * as OpentronsSharedData from '@opentrons/shared-data' -import type * as OpentronsComponents from '@opentrons/components' -import type * as OpentronsApiClient from '@opentrons/api-client' - -const mockNavigate = vi.fn() - -vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() - return { - ...reactRouterDom, - useNavigate: () => mockNavigate, - } -}) - -vi.mock('@opentrons/components', async importOriginal => { - const actual = await importOriginal() - return { - ...actual, - Tooltip: vi.fn(({ children }) =>
{children}
), - } -}) - -vi.mock('@opentrons/shared-data', async importOriginal => { - const actual = await importOriginal() - return { - ...actual, - getPipetteModelSpecs: vi.fn(), - } -}) - -vi.mock('@opentrons/react-api-client') -vi.mock('../../../../organisms/ProtocolUpload/hooks') -vi.mock('../../../../organisms/RunDetails/ConfirmCancelModal') -vi.mock('../../../../organisms/RunTimeControl/hooks') -vi.mock('../../hooks') -vi.mock('../../HeaterShakerIsRunningModal') -vi.mock('../../../ModuleCard/ConfirmAttachmentModal') -vi.mock('../../../ModuleCard/hooks') -vi.mock('../../../RunProgressMeter') -vi.mock('../../../../redux/analytics') -vi.mock('../../../../redux/config') -vi.mock('../RunFailedModal') -vi.mock('../../../../redux/robot-update/selectors') -vi.mock('../../../../redux/robot-settings/selectors') -vi.mock('../../../DropTipWizardFlows') -vi.mock('../../../../resources/deck_configuration/utils') -vi.mock('../../../../resources/deck_configuration/hooks') -vi.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') -vi.mock('../../../ProtocolUpload/hooks/useMostRecentRunId') -vi.mock('../../../../resources/runs') -vi.mock('../../../ErrorRecoveryFlows') -vi.mock('../ProtocolDropTipModal') -vi.mock('../ConfirmMissingStepsModal') - -const ROBOT_NAME = 'otie' -const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b' -const CREATED_AT = '03/03/2022 19:08:49' -const STARTED_AT = '2022-03-03T19:09:40.620530+00:00' -const COMPLETED_AT = '2022-03-03T19:39:53.620530+00:00' -const PROTOCOL_NAME = 'A Protocol for Otie' -const mockSettings = { - id: 'enableDoorSafetySwitch', - title: 'Enable Door Safety Switch', - description: '', - value: true, - restart_required: false, -} -const MOCK_ROTOCOL_LIQUID_KEY = { liquids: [] } -const MOCK_ROBOT_SERIAL_NUMBER = 'OT123' - -const simpleV6Protocol = (_uncastedSimpleV6Protocol as unknown) as OpentronsSharedData.CompletedProtocolAnalysis - -const PROTOCOL_DETAILS = { - displayName: PROTOCOL_NAME, - protocolData: simpleV6Protocol, - protocolKey: 'fakeProtocolKey', - isProtocolAnalyzing: false, - robotType: 'OT-2 Standard' as const, -} - -const RUN_COMMAND_ERRORS = { - data: { - data: [ - { - errorCode: '4000', - errorType: 'test', - isDefined: false, - createdAt: '9-9-9', - detail: 'blah blah', - id: '123', - }, - ], - meta: { - cursor: 0, - pageLength: 1, - }, - }, -} as any - -const mockMovingHeaterShaker = { - id: 'heatershaker_id', - moduleModel: 'heaterShakerModuleV1', - moduleType: 'heaterShakerModuleType', - serialNumber: 'jkl123', - hardwareRevision: 'heatershaker_v4.0', - firmwareVersion: 'v2.0.0', - hasAvailableUpdate: true, - data: { - labwareLatchStatus: 'idle_closed', - speedStatus: 'speeding up', - temperatureStatus: 'idle', - currentSpeed: null, - currentTemperature: null, - targetSpeed: null, - targetTemp: null, - errorDetails: null, - status: 'idle', - }, - usbPort: { path: '/dev/ot_module_heatershaker0', port: 1 }, -} as any - -const mockEstopStatus = { - data: { - status: DISENGAGED, - leftEstopPhysicalStatus: DISENGAGED, - rightEstopPhysicalStatus: NOT_PRESENT, - }, -} -const mockDoorStatus = { - data: { - status: 'closed', - doorRequiredClosedForProtocol: true, - }, -} -let mockMissingSteps: MissingSteps = [] - -const render = () => { - return renderWithProviders( - - vi.fn())} - missingSetupSteps={mockMissingSteps} - /> - , - { i18nInstance: i18n } - ) -} -let mockTrackEvent: Mock -let mockTrackProtocolRunEvent: Mock -let mockCloseCurrentRun: Mock -let mockDetermineTipStatus: Mock - -describe('ProtocolRunHeader', () => { - beforeEach(() => { - mockTrackEvent = vi.fn() - mockTrackProtocolRunEvent = vi.fn(() => new Promise(resolve => resolve({}))) - mockCloseCurrentRun = vi.fn() - mockDetermineTipStatus = vi.fn() - mockMissingSteps = [] - vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) - vi.mocked(ConfirmCancelModal).mockReturnValue( -
Mock ConfirmCancelModal
- ) - vi.mocked(RunProgressMeter).mockReturnValue( -
Mock RunProgressMeter
- ) - vi.mocked(HeaterShakerIsRunningModal).mockReturnValue( -
Mock HeaterShakerIsRunningModal
- ) - vi.mocked(useModulesQuery).mockReturnValue({ - data: { data: [] }, - } as any) - vi.mocked(usePipettesQuery).mockReturnValue({ - data: { - data: { - left: null, - right: null, - }, - }, - } as any) - vi.mocked(getIsHeaterShakerAttached).mockReturnValue(false) - vi.mocked(useIsRobotViewable).mockReturnValue(true) - vi.mocked(ConfirmAttachmentModal).mockReturnValue( -
mock confirm attachment modal
- ) - vi.mocked(ConfirmMissingStepsModal).mockReturnValue( -
mock missing steps modal
- ) - when(vi.mocked(useProtocolAnalysisErrors)).calledWith(RUN_ID).thenReturn({ - analysisErrors: null, - }) - vi.mocked(useIsHeaterShakerInProtocol).mockReturnValue(false) - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'reinstall', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) - when(vi.mocked(useCurrentRunId)).calledWith().thenReturn(RUN_ID) - when(vi.mocked(useCloseCurrentRun)).calledWith().thenReturn({ - isClosingCurrentRun: false, - closeCurrentRun: mockCloseCurrentRun, - }) - when(vi.mocked(useRunControls)) - .calledWith(RUN_ID, expect.anything()) - .thenReturn({ - play: () => {}, - pause: () => {}, - stop: () => {}, - reset: () => {}, - resumeFromRecovery: () => {}, - isPlayRunActionLoading: false, - isPauseRunActionLoading: false, - isStopRunActionLoading: false, - isResetRunLoading: false, - isResumeRunFromRecoveryActionLoading: false, - }) - when(vi.mocked(useRunStatus)).calledWith(RUN_ID).thenReturn(RUN_STATUS_IDLE) - when(vi.mocked(useRunTimestamps)).calledWith(RUN_ID).thenReturn({ - startedAt: STARTED_AT, - pausedAt: null, - stoppedAt: null, - completedAt: null, - }) - when(vi.mocked(useRunCreatedAtTimestamp)) - .calledWith(RUN_ID) - .thenReturn(CREATED_AT) - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID, { staleTime: Infinity }) - .thenReturn({ - data: { data: mockIdleUnstartedRun }, - } as UseQueryResult) - when(vi.mocked(useProtocolDetailsForRun)) - .calledWith(RUN_ID) - .thenReturn(PROTOCOL_DETAILS) - when(vi.mocked(useTrackProtocolRunEvent)) - .calledWith(RUN_ID, ROBOT_NAME) - .thenReturn({ - trackProtocolRunEvent: mockTrackProtocolRunEvent, - }) - when(vi.mocked(useDismissCurrentRunMutation)) - .calledWith() - .thenReturn({ - dismissCurrentRun: vi.fn(), - } as any) - when(vi.mocked(useUnmatchedModulesForProtocol)) - .calledWith(ROBOT_NAME, RUN_ID) - .thenReturn({ missingModuleIds: [], remainingAttachedModules: [] }) - when(vi.mocked(useRunCalibrationStatus)) - .calledWith(ROBOT_NAME, RUN_ID) - .thenReturn({ complete: true }) - when(vi.mocked(useIsFlex)).calledWith(ROBOT_NAME).thenReturn(true) - when(vi.mocked(useModuleCalibrationStatus)) - .calledWith(ROBOT_NAME, RUN_ID) - .thenReturn({ complete: true }) - vi.mocked(RunFailedModal).mockReturnValue(
mock RunFailedModal
) - vi.mocked(useEstopQuery).mockReturnValue({ data: mockEstopStatus } as any) - vi.mocked(useDoorQuery).mockReturnValue({ data: mockDoorStatus } as any) - vi.mocked(getRobotSettings).mockReturnValue([mockSettings]) - vi.mocked(useInstrumentsQuery).mockReturnValue({ data: {} } as any) - vi.mocked(useHost).mockReturnValue({} as any) - vi.mocked(useTipAttachmentStatus).mockReturnValue({ - aPipetteWithTip: instrumentsResponseLeftPipetteFixture, - areTipsAttached: true, - determineTipStatus: mockDetermineTipStatus, - resetTipStatus: vi.fn(), - } as any) - vi.mocked(useDropTipWizardFlows).mockReturnValue({ - showDTWiz: false, - toggleDTWiz: vi.fn(), - }) - vi.mocked(getPipetteModelSpecs).mockReturnValue('p10_single_v1' as any) - when(vi.mocked(useMostRecentCompletedAnalysis)) - .calledWith(RUN_ID) - .thenReturn({ - ...noModulesProtocol, - ...MOCK_ROTOCOL_LIQUID_KEY, - } as any) - vi.mocked(useRunCommandErrors).mockReturnValue(RUN_COMMAND_ERRORS) - vi.mocked(useDeckConfigurationCompatibility).mockReturnValue([]) - vi.mocked(getIsFixtureMismatch).mockReturnValue(false) - vi.mocked(useMostRecentRunId).mockReturnValue(RUN_ID) - vi.mocked(useRobot).mockReturnValue({ - ...mockConnectableRobot, - health: { - ...mockConnectableRobot.health, - robot_serial: MOCK_ROBOT_SERIAL_NUMBER, - }, - }) - vi.mocked(useErrorRecoveryFlows).mockReturnValue({ - isERActive: false, - failedCommand: {}, - } as any) - vi.mocked(ErrorRecoveryFlows).mockReturnValue( -
MOCK_ERROR_RECOVERY
- ) - vi.mocked(useProtocolDropTipModal).mockReturnValue({ - onDTModalRemoval: vi.fn(), - onDTModalSkip: vi.fn(), - showDTModal: false, - isDisabled: false, - }) - vi.mocked(ProtocolDropTipModal).mockReturnValue( -
MOCK_DROP_TIP_MODAL
- ) - vi.mocked(DropTipWizardFlows).mockReturnValue( -
MOCK_DROP_TIP_WIZARD_FLOWS
- ) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - it('renders a protocol name, run record id, status, and run time', () => { - render() - - screen.getByText('A Protocol for Otie') - screen.getByText('Run') - screen.getByText('03/03/2022 19:08:49') - screen.getByText('Status') - screen.getByText('Not started') - screen.getByText('Run Time') - }) - - it('links to a protocol details page', () => { - render() - - const protocolNameLink = screen.getByRole('link', { - name: 'A Protocol for Otie', - }) - expect(protocolNameLink.getAttribute('href')).toBe( - `/protocols/${PROTOCOL_DETAILS.protocolKey}` - ) - }) - - it('does not render link to protocol detail page if protocol key is absent', () => { - when(vi.mocked(useProtocolDetailsForRun)) - .calledWith(RUN_ID) - .thenReturn({ ...PROTOCOL_DETAILS, protocolKey: null }) - render() - - expect( - screen.queryByRole('link', { name: 'A Protocol for Otie' }) - ).toBeNull() - }) - - it('renders a disabled "Analyzing on robot" button if robot-side analysis is not complete', () => { - when(vi.mocked(useProtocolDetailsForRun)).calledWith(RUN_ID).thenReturn({ - displayName: null, - protocolData: null, - protocolKey: null, - isProtocolAnalyzing: true, - robotType: 'OT-2 Standard', - }) - - render() - - const button = screen.getByRole('button', { name: 'Analyzing on robot' }) - expect(button).toBeDisabled() - }) - - it('renders a start run button and cancel run button when run is ready to start', () => { - render() - - screen.getByRole('button', { name: 'Start run' }) - screen.queryByText(formatTimestamp(STARTED_AT)) - screen.queryByText('Protocol start') - screen.queryByText('Protocol end') - fireEvent.click(screen.getByRole('button', { name: 'Cancel run' })) - screen.getByText('Mock ConfirmCancelModal') - screen.getByText('Mock RunProgressMeter') - }) - - it('calls trackProtocolRunEvent when start run button clicked', () => { - render() - - const button = screen.getByRole('button', { name: 'Start run' }) - fireEvent.click(button) - expect(mockTrackProtocolRunEvent).toBeCalledTimes(1) - expect(mockTrackProtocolRunEvent).toBeCalledWith({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.START, - properties: {}, - }) - }) - - it('dismisses a current but canceled run and calls trackProtocolRunEvent', () => { - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_STOPPED) - vi.mocked(useNotifyRunQuery).mockReturnValue({ - data: { data: { ...mockIdleUnstartedRun, current: true } }, - } as UseQueryResult) - render() - expect(mockTrackProtocolRunEvent).toBeCalled() - expect(mockTrackProtocolRunEvent).toBeCalledWith({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.FINISH, - properties: {}, - }) - }) - - it('disables the Start Run button with tooltip if calibration is incomplete', () => { - when(vi.mocked(useRunCalibrationStatus)) - .calledWith(ROBOT_NAME, RUN_ID) - .thenReturn({ complete: false }) - - render() - - const button = screen.getByRole('button', { name: 'Start run' }) - expect(button).toBeDisabled() - screen.getByText('Complete required steps in Setup tab') - }) - - it('disables the Start Run button with tooltip if a module is missing', () => { - when(vi.mocked(useUnmatchedModulesForProtocol)) - .calledWith(ROBOT_NAME, RUN_ID) - .thenReturn({ - missingModuleIds: ['temperatureModuleV1'], - remainingAttachedModules: [], - }) - - render() - const button = screen.getByRole('button', { name: 'Start run' }) - expect(button).toBeDisabled() - screen.getByText('Complete required steps in Setup tab') - }) - - it('disables the Start Run button with tooltip if robot software update is available', () => { - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) - - render() - const button = screen.getByRole('button', { name: 'Start run' }) - expect(button).toBeDisabled() - screen.getByText( - 'A software update is available for this robot. Update to run protocols.' - ) - }) - - it('disables the Start Run button when a fixture is not configured or conflicted', () => { - vi.mocked(useDeckConfigurationCompatibility).mockReturnValue([ - { - cutoutId: 'cutoutA1', - cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, - requiredAddressableAreas: ['D4'], - compatibleCutoutFixtureIds: [ - STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, - ], - missingLabwareDisplayName: null, - }, - ]) - vi.mocked(getIsFixtureMismatch).mockReturnValue(true) - render() - const button = screen.getByRole('button', { name: 'Start run' }) - expect(button).toBeDisabled() - }) - - it('renders a pause run button, start time, and end time when run is running, and calls trackProtocolRunEvent when button clicked', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockRunningRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_RUNNING) - render() - - const button = screen.getByRole('button', { name: 'Pause run' }) - screen.getByText(formatTimestamp(STARTED_AT)) - screen.getByText('Protocol start') - screen.getByText('Protocol end') - fireEvent.click(button) - expect(mockTrackProtocolRunEvent).toBeCalledWith({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.PAUSE, - }) - }) - - it('renders a cancel run button when running and shows a confirm cancel modal when clicked', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockRunningRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_RUNNING) - render() - - expect(screen.queryByText('Mock ConfirmCancelModal')).toBeFalsy() - const cancelButton = screen.getByText('Cancel run') - fireEvent.click(cancelButton) - screen.getByText('Mock ConfirmCancelModal') - }) - - it('renders a Resume Run button and Cancel Run button when paused and call trackProtocolRunEvent when resume button clicked', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockPausedRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_PAUSED) - - render() - - const button = screen.getByRole('button', { name: 'Resume run' }) - screen.getByRole('button', { name: 'Cancel run' }) - screen.getByText('Paused') - fireEvent.click(button) - expect(mockTrackProtocolRunEvent).toBeCalledWith({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.RESUME, - properties: {}, - }) - }) - - it('renders a disabled Canceling Run button and when stop requested', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockStopRequestedRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_STOP_REQUESTED) - - render() - - const button = screen.getByRole('button', { name: 'Canceling Run' }) - expect(button).toBeDisabled() - screen.getByText('Stop requested') - }) - - it('renders a disabled button and when the robot door is open', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockRunningRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_BLOCKED_BY_OPEN_DOOR) - - const mockOpenDoorStatus = { - data: { status: 'open', doorRequiredClosedForProtocol: true }, - } - vi.mocked(useDoorQuery).mockReturnValue({ data: mockOpenDoorStatus } as any) - - render() - - const button = screen.getByRole('button', { name: 'Resume run' }) - expect(button).toBeDisabled() - screen.getByText('Close robot door') - }) - - it('renders a Run Again button and end time when run has stopped and calls trackProtocolRunEvent when run again button clicked', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockStoppedRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_STOPPED) - when(vi.mocked(useRunTimestamps)).calledWith(RUN_ID).thenReturn({ - startedAt: STARTED_AT, - pausedAt: null, - stoppedAt: null, - completedAt: COMPLETED_AT, - }) - - render() - - const button = screen.getByText('Run again') - screen.getByText('Canceled') - screen.getByText(formatTimestamp(COMPLETED_AT)) - fireEvent.click(button) - expect(mockTrackProtocolRunEvent).toBeCalledWith({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.AGAIN, - }) - }) - - it('renders a Run Again button and end time when run has failed and calls trackProtocolRunEvent when run again button clicked', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockFailedRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_FAILED) - when(vi.mocked(useRunTimestamps)).calledWith(RUN_ID).thenReturn({ - startedAt: STARTED_AT, - pausedAt: null, - stoppedAt: null, - completedAt: COMPLETED_AT, - }) - - render() - - const button = screen.getByText('Run again') - screen.getByText('Failed') - screen.getByText(formatTimestamp(COMPLETED_AT)) - fireEvent.click(button) - expect(mockTrackProtocolRunEvent).toBeCalledWith({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.AGAIN, - }) - }) - - it('renders a Run Again button and end time when run has succeeded and calls trackProtocolRunEvent when run again button clicked', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockSucceededRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_SUCCEEDED) - when(vi.mocked(useRunTimestamps)).calledWith(RUN_ID).thenReturn({ - startedAt: STARTED_AT, - pausedAt: null, - stoppedAt: null, - completedAt: COMPLETED_AT, - }) - - render() - - const button = screen.getByText('Run again') - screen.getByText('Completed') - screen.getByText(formatTimestamp(COMPLETED_AT)) - fireEvent.click(button) - expect(mockTrackEvent).toBeCalledWith({ - name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, - properties: { - sourceLocation: 'RunRecordDetail', - robotSerialNumber: MOCK_ROBOT_SERIAL_NUMBER, - }, - }) - expect(mockTrackProtocolRunEvent).toBeCalledWith({ - name: ANALYTICS_PROTOCOL_RUN_ACTION.AGAIN, - }) - }) - - it('disables the Run Again button with tooltip for a completed run if the robot is busy', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockSucceededRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_SUCCEEDED) - when(vi.mocked(useRunTimestamps)).calledWith(RUN_ID).thenReturn({ - startedAt: STARTED_AT, - pausedAt: null, - stoppedAt: null, - completedAt: COMPLETED_AT, - }) - when(vi.mocked(useCurrentRunId)) - .calledWith() - .thenReturn('some other run id') - - render() - - const button = screen.getByRole('button', { name: 'Run again' }) - expect(button).toBeDisabled() - screen.getByText('Robot is busy') - }) - - it('renders an alert when the robot door is open', () => { - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_BLOCKED_BY_OPEN_DOOR) - render() - - screen.getByText('Close robot door to resume run') - }) - - it('renders a error detail link banner when run has failed', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockFailedRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_FAILED) - render() - - fireEvent.click(screen.getByText('View error details')) - screen.getByText('mock RunFailedModal') - }) - - it('does not render banners when a run is resetting', () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: mockFailedRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_FAILED) - when(vi.mocked(useRunControls)) - .calledWith(RUN_ID, expect.anything()) - .thenReturn({ - play: () => {}, - pause: () => {}, - stop: () => {}, - reset: () => {}, - resumeFromRecovery: () => {}, - isPlayRunActionLoading: false, - isPauseRunActionLoading: false, - isStopRunActionLoading: false, - isResetRunLoading: true, - isResumeRunFromRecoveryActionLoading: false, - }) - render() - - expect(screen.queryByText('mock RunFailedModal')).not.toBeInTheDocument() - }) - - it('renders a clear protocol banner when run has been canceled', () => { - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_STOPPED) - vi.mocked(useTipAttachmentStatus).mockReturnValue({ - areTipsAttached: false, - determineTipStatus: mockDetermineTipStatus, - } as any) - render() - - screen.getByText('Run canceled.') - expect(screen.queryByTestId('Banner_close-button')).not.toBeInTheDocument() - }) - - it('renders a clear protocol banner when run has succeeded', async () => { - vi.mocked(useNotifyRunQuery).mockReturnValue({ - data: { data: mockSucceededRun }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_SUCCEEDED) - render() - - screen.getByText('Run completed with warnings.') - }) - - it('does not display the "run successful" banner if the successful run is not current', async () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { data: { ...mockSucceededRun, current: false } }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_SUCCEEDED) - render() - - expect(screen.queryByText('Run completed.')).not.toBeInTheDocument() - }) - - it('if a heater shaker is shaking, clicking on start run should render HeaterShakerIsRunningModal', async () => { - when(vi.mocked(useRunStatus)).calledWith(RUN_ID).thenReturn(RUN_STATUS_IDLE) - vi.mocked(useIsHeaterShakerInProtocol).mockReturnValue(true) - vi.mocked(useModulesQuery).mockReturnValue({ - data: { data: [mockMovingHeaterShaker] }, - } as any) - render() - const button = screen.getByRole('button', { name: 'Start run' }) - fireEvent.click(button) - await waitFor(() => { - screen.getByText('Mock HeaterShakerIsRunningModal') - }) - }) - - it('does not render the confirm attachment modal when there is a heater shaker in the protocol and run is idle', () => { - vi.mocked(useModulesQuery).mockReturnValue({ - data: { data: [mockHeaterShaker] }, - } as any) - vi.mocked(useIsHeaterShakerInProtocol).mockReturnValue(true) - render() - - const button = screen.getByRole('button', { name: 'Start run' }) - fireEvent.click(button) - screen.getByText('mock confirm attachment modal') - expect(mockTrackProtocolRunEvent).toBeCalledTimes(0) - }) - - it('renders the confirm attachment modal when there is a heater shaker in the protocol and the heater shaker is idle status and run is paused', () => { - when(vi.mocked(useRunStatus)) - .calledWith(RUN_ID) - .thenReturn(RUN_STATUS_PAUSED) - - vi.mocked(useModulesQuery).mockReturnValue({ - data: { data: [mockHeaterShaker] }, - } as any) - vi.mocked(useIsHeaterShakerInProtocol).mockReturnValue(true) - render() - - const button = screen.getByRole('button', { name: 'Resume run' }) - fireEvent.click(button) - expect(screen.queryByText('mock confirm attachment modal')).toBeFalsy() - expect(mockTrackProtocolRunEvent).toBeCalledTimes(1) - }) - - it('does NOT render confirm attachment modal when the user already confirmed the heater shaker is attached', () => { - vi.mocked(getIsHeaterShakerAttached).mockReturnValue(true) - vi.mocked(useModulesQuery).mockReturnValue({ - data: { data: [mockHeaterShaker] }, - } as any) - vi.mocked(useIsHeaterShakerInProtocol).mockReturnValue(true) - render() - const button = screen.getByRole('button', { name: 'Start run' }) - fireEvent.click(button) - expect(vi.mocked(useRunControls)).toHaveBeenCalled() - }) - - it('renders analysis error modal if there is an analysis error', () => { - when(vi.mocked(useProtocolAnalysisErrors)) - .calledWith(RUN_ID) - .thenReturn({ - analysisErrors: [ - { - id: 'error_id', - detail: 'protocol analysis error', - errorType: 'analysis', - createdAt: '100000', - }, - ], - }) - render() - screen.getByText('protocol analysis error') - }) - - it('renders analysis error banner if there is an analysis error', () => { - when(vi.mocked(useProtocolAnalysisErrors)) - .calledWith(RUN_ID) - .thenReturn({ - analysisErrors: [ - { - id: 'error_id', - detail: 'protocol analysis error', - errorType: 'analysis', - createdAt: '100000', - }, - ], - }) - render() - screen.getByText('Protocol analysis failed.') - }) - - it('renders the devices page when robot is not viewable but protocol is loaded', async () => { - vi.mocked(useIsRobotViewable).mockReturnValue(false) - render() - await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith('/devices') - }) - }) - - it('renders door close banner when the robot door is open', () => { - const mockOpenDoorStatus = { - data: { status: 'open', doorRequiredClosedForProtocol: true }, - } - vi.mocked(useDoorQuery).mockReturnValue({ data: mockOpenDoorStatus } as any) - render() - screen.getByText('Close the robot door before starting the run.') - }) - - it('should render door close banner when door is open and enabled safety door switch is on - OT-2', () => { - when(vi.mocked(useIsFlex)).calledWith(ROBOT_NAME).thenReturn(false) - const mockOpenDoorStatus = { - data: { status: 'open', doorRequiredClosedForProtocol: true }, - } - vi.mocked(useDoorQuery).mockReturnValue({ data: mockOpenDoorStatus } as any) - render() - screen.getByText('Close the robot door before starting the run.') - }) - - it('should not render door close banner when door is open and enabled safety door switch is off - OT-2', () => { - when(vi.mocked(useIsFlex)).calledWith(ROBOT_NAME).thenReturn(false) - const mockOffSettings = { ...mockSettings, value: false } - vi.mocked(getRobotSettings).mockReturnValue([mockOffSettings]) - const mockOpenDoorStatus = { - data: { status: 'open', doorRequiredClosedForProtocol: true }, - } - vi.mocked(useDoorQuery).mockReturnValue({ data: mockOpenDoorStatus } as any) - render() - expect( - screen.queryByText('Close the robot door before starting the run.') - ).not.toBeInTheDocument() - }) - - it('renders the drop tip modal initially when the run ends if tips are attached', () => { - vi.mocked(useProtocolDropTipModal).mockReturnValue({ - onDTModalRemoval: vi.fn(), - onDTModalSkip: vi.fn(), - showDTModal: true, - isDisabled: false, - }) - - render() - - screen.getByText('MOCK_DROP_TIP_MODAL') - }) - - it('does not render the drop tip modal when the run is not over', async () => { - when(vi.mocked(useNotifyRunQuery)) - .calledWith(RUN_ID) - .thenReturn({ - data: { - data: { - ...mockIdleUnstartedRun, - current: true, - status: RUN_STATUS_IDLE, - }, - }, - } as UseQueryResult) - when(vi.mocked(useRunStatus)).calledWith(RUN_ID).thenReturn(RUN_STATUS_IDLE) - - render() - await waitFor(() => { - expect(mockDetermineTipStatus).not.toHaveBeenCalled() - }) - }) - - it('renders Error Recovery Flows when isERActive is true', () => { - vi.mocked(useErrorRecoveryFlows).mockReturnValue({ - isERActive: true, - failedCommand: {}, - } as any) - - render() - screen.getByText('MOCK_ERROR_RECOVERY') - }) - - it('renders DropTipWizardFlows when conditions are met', () => { - vi.mocked(useDropTipWizardFlows).mockReturnValue({ - showDTWiz: true, - toggleDTWiz: vi.fn(), - }) - - render() - screen.getByText('MOCK_DROP_TIP_WIZARD_FLOWS') - }) -}) diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx index 777c263078d..1ea639a0b2e 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx @@ -8,7 +8,10 @@ import { i18n } from '../../../../i18n' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useRunStatus } from '../../../RunTimeControl/hooks' import { useNotifyRunQuery } from '../../../../resources/runs' -import { mockSucceededRun } from '../../../RunTimeControl/__fixtures__' +import { + mockSucceededRun, + mockIdleUnstartedRun, +} from '../../../RunTimeControl/__fixtures__' import { ProtocolRunRuntimeParameters } from '../ProtocolRunRunTimeParameters' import type { UseQueryResult } from 'react-query' @@ -128,6 +131,13 @@ describe('ProtocolRunRuntimeParameters', () => { }) it('should render title, and banner when RunTimeParameters are not empty and all values are default', () => { + when(useNotifyRunQuery) + .calledWith(RUN_ID) + .thenReturn({ + data: { + data: mockIdleUnstartedRun, + }, + } as any) render(props) screen.getByText('Parameters') screen.getByText('Default values') @@ -151,6 +161,13 @@ describe('ProtocolRunRuntimeParameters', () => { }, ], } as CompletedProtocolAnalysis) + when(useNotifyRunQuery) + .calledWith(RUN_ID) + .thenReturn({ + data: { + data: mockIdleUnstartedRun, + }, + } as any) render(props) screen.getByText('Parameters') screen.getByText('Custom values') @@ -160,6 +177,39 @@ describe('ProtocolRunRuntimeParameters', () => { screen.getByText('Value') }) + it('should render title, and banner when RunTimeParameters from view protocol run record overflow menu button', () => { + when(useNotifyRunQuery) + .calledWith(RUN_ID) + .thenReturn({ + data: { + data: { + ...mockSucceededRun, + runTimeParameters: mockRunTimeParameterData, + }, + }, + } as any) + vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue({ + runTimeParameters: [ + ...mockRunTimeParameterData, + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'bool', + default: false, + value: true, + }, + ], + } as CompletedProtocolAnalysis) + + vi.mocked(useRunStatus).mockReturnValue('succeeded') + render(props) + screen.getByText('Download files') + screen.getByText( + 'All files associated with the protocol run are available on the robot detail screen.' + ) + }) + it('should render RunTimeParameters when RunTimeParameters are not empty', () => { render(props) screen.getByText('Dry Run') diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/SetupTipLengthCalibrationButton.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/SetupTipLengthCalibrationButton.test.tsx index 993e14080f5..1ef12c372f8 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/SetupTipLengthCalibrationButton.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/SetupTipLengthCalibrationButton.test.tsx @@ -10,7 +10,7 @@ import { i18n } from '../../../../i18n' import { mockDeckCalData } from '../../../../redux/calibration/__fixtures__' import { mockTipLengthCalLauncher } from '../../hooks/__fixtures__/taskListFixtures' import { useDeckCalibrationData, useRunHasStarted } from '../../hooks' -import { useDashboardCalibrateTipLength } from '../../../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' +import { useDashboardCalibrateTipLength } from '../../../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' import { SetupTipLengthCalibrationButton } from '../SetupTipLengthCalibrationButton' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -18,7 +18,7 @@ import type { LabwareDefinition2 } from '@opentrons/shared-data' vi.mock('@opentrons/components/src/hooks') vi.mock('../../../../organisms/RunTimeControl/hooks') vi.mock( - '../../../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' + '../../../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' ) vi.mock('../../../../redux/config') vi.mock('../../../../redux/sessions/selectors') diff --git a/app/src/organisms/Devices/RecentProtocolRuns.tsx b/app/src/organisms/Devices/RecentProtocolRuns.tsx index 06815a7b064..787987c4d70 100644 --- a/app/src/organisms/Devices/RecentProtocolRuns.tsx +++ b/app/src/organisms/Devices/RecentProtocolRuns.tsx @@ -35,6 +35,12 @@ export function RecentProtocolRuns({ const currentRunId = useCurrentRunId() const { isRunTerminal } = useRunStatuses() const robotIsBusy = currentRunId != null ? !isRunTerminal : false + const nonQuickTransferRuns = runs?.filter(run => { + const protocol = protocols?.data?.data.find( + protocol => protocol.id === run.protocolId + ) + return protocol?.protocolKind !== 'quick-transfer' + }) return ( - {isRobotViewable && runs && runs.length > 0 && ( - <> - - - {t('run')} - - - {t('protocol')} - - - {t('files')} - - 0 && ( + <> + - {t('status')} - - - {t('run_duration')} - - - {runs - .sort( - (a, b) => - new Date(b.createdAt).getTime() - - new Date(a.createdAt).getTime() - ) - .map((run, index) => { - const protocol = protocols?.data?.data.find( - protocol => protocol.id === run.protocolId + + {t('run')} + + + {t('protocol')} + + + {t('files')} + + + {t('status')} + + + {t('run_duration')} + + + {nonQuickTransferRuns + .sort( + (a, b) => + new Date(b.createdAt).getTime() - + new Date(a.createdAt).getTime() ) - const protocolName = - protocol?.metadata.protocolName ?? - protocol?.files[0].name ?? - t('shared:loading') ?? - '' + .map((run, index) => { + const protocol = protocols?.data?.data.find( + protocol => protocol.id === run.protocolId + ) + const protocolName = + protocol?.metadata.protocolName ?? + protocol?.files[0].name ?? + t('shared:loading') ?? + '' - return ( - - ) - })} - - )} + return ( + + ) + })} + + )} {!isRobotViewable && ( )} - {isRobotViewable && (runs == null || runs.length === 0) && ( - - {t('no_protocol_runs')} - - )} + {isRobotViewable && + (nonQuickTransferRuns == null || + nonQuickTransferRuns.length === 0) && ( + + {t('no_protocol_runs')} + + )} ) diff --git a/app/src/organisms/Devices/RobotOverflowMenu.tsx b/app/src/organisms/Devices/RobotOverflowMenu.tsx index 7d13b3cc0a0..35363f2d16b 100644 --- a/app/src/organisms/Devices/RobotOverflowMenu.tsx +++ b/app/src/organisms/Devices/RobotOverflowMenu.tsx @@ -1,8 +1,9 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { Link } from 'react-router-dom' +import { css } from 'styled-components' import { ALIGN_FLEX_END, @@ -21,7 +22,7 @@ import { } from '@opentrons/components' import { CONNECTABLE, removeRobot } from '../../redux/discovery' -import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../redux/robot-update' import { Divider } from '../../atoms/structure' import { getTopPortalEl } from '../../App/portal' import { ChooseProtocolSlideout } from '../ChooseProtocolSlideout' @@ -31,8 +32,7 @@ import { useIsRobotBusy } from './hooks' import type { StyleProps } from '@opentrons/components' import type { DiscoveredRobot } from '../../redux/discovery/types' -import type { Dispatch, State } from '../../redux/types' -import { css } from 'styled-components' +import type { Dispatch } from '../../redux/types' interface RobotOverflowMenuProps extends StyleProps { robot: DiscoveredRobot @@ -59,11 +59,9 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { setShowConnectionTroubleshootingModal, ] = React.useState(false) - const { autoUpdateAction } = useSelector((state: State) => { - return getRobotUpdateDisplayInfo(state, robot.name) - }) - const isRobotOnWrongVersionOfSoftware = - autoUpdateAction === 'upgrade' || autoUpdateAction === 'downgrade' + const isRobotOnWrongVersionOfSoftware = useIsRobotOnWrongVersionOfSoftware( + robot.name + ) const isRobotBusy = useIsRobotBusy({ poll: true }) diff --git a/app/src/organisms/Devices/RobotOverviewOverflowMenu.tsx b/app/src/organisms/Devices/RobotOverviewOverflowMenu.tsx index 6ae90f8a1c1..e1e571ec858 100644 --- a/app/src/organisms/Devices/RobotOverviewOverflowMenu.tsx +++ b/app/src/organisms/Devices/RobotOverviewOverflowMenu.tsx @@ -3,7 +3,7 @@ import { css } from 'styled-components' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { BORDERS, @@ -25,7 +25,7 @@ import { Divider } from '../../atoms/structure' import { ChooseProtocolSlideout } from '../../organisms/ChooseProtocolSlideout' import { DisconnectModal } from '../../organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal' import { handleUpdateBuildroot } from '../../organisms/Devices/RobotSettings/UpdateBuildroot' -import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../redux/robot-update' import { UNREACHABLE, CONNECTABLE, REACHABLE } from '../../redux/discovery' import { checkShellUpdate } from '../../redux/shell' import { restartRobot } from '../../redux/robot-admin' @@ -35,7 +35,7 @@ import { useCanDisconnect } from '../../resources/networking/hooks' import { useIsEstopNotDisengaged } from '../../resources/devices/hooks/useIsEstopNotDisengaged' import { useCurrentRunId } from '../../resources/runs' import type { DiscoveredRobot } from '../../redux/discovery/types' -import type { Dispatch, State } from '../../redux/types' +import type { Dispatch } from '../../redux/types' interface RobotOverviewOverflowMenuProps { robot: DiscoveredRobot @@ -90,11 +90,9 @@ export const RobotOverviewOverflowMenu = ( setShowChooseProtocolSlideout(true) } - const { autoUpdateAction } = useSelector((state: State) => { - return getRobotUpdateDisplayInfo(state, robot.name) - }) - const isRobotOnWrongVersionOfSoftware = - autoUpdateAction === 'upgrade' || autoUpdateAction === 'downgrade' + const isRobotOnWrongVersionOfSoftware = useIsRobotOnWrongVersionOfSoftware( + robot.name + ) const isRobotUnavailable = isRobotBusy || robot?.status !== CONNECTABLE const isUpdateSoftwareItemVisible = isRobotOnWrongVersionOfSoftware && diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx index 39ad04dc044..f51d4292f71 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx @@ -7,6 +7,7 @@ import last from 'lodash/last' import { GET, request } from '@opentrons/api-client' import { ALIGN_CENTER, + ALIGN_END, Box, Flex, JUSTIFY_SPACE_BETWEEN, @@ -131,6 +132,7 @@ export function Troubleshooting({ marginLeft={SPACING_AUTO} onClick={handleClick} id="AdvancedSettings_downloadLogsButton" + alignSelf={ALIGN_END} > {t('download_logs')} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx index e5564e88385..5308068cc5a 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx @@ -8,18 +8,21 @@ import { Box, Flex, JUSTIFY_SPACE_BETWEEN, - LegacyStyledText, + DIRECTION_COLUMN, SPACING_AUTO, SPACING, + LegacyStyledText, Tooltip, TYPOGRAPHY, useHoverTooltip, + StyledText, } from '@opentrons/components' import { ExternalLink } from '../../../../atoms/Link/ExternalLink' import { TertiaryButton } from '../../../../atoms/buttons' import { getRobotUpdateDisplayInfo } from '../../../../redux/robot-update' import { useDispatchStartRobotUpdate } from '../../../../redux/robot-update/hooks' +import { Banner } from '../../../../atoms/Banner' import type { State } from '../../../../redux/types' @@ -67,43 +70,50 @@ export function UpdateRobotSoftware({ } return ( - - - + + + + {t('update_robot_software')} + + + {t('branded:update_robot_software_description')} + + + {t('branded:update_robot_software_link')} + + + - {t('update_robot_software')} - - - {t('branded:update_robot_software_description')} - - - {t('branded:update_robot_software_link')} - - - - {t('browse_file_system')} - - - {updateFromFileDisabledReason != null && ( - - {updateFromFileDisabledReason} - - )} + {t('browse_file_system')} + + + {updateFromFileDisabledReason != null && ( + + {updateFromFileDisabledReason} + + )} + + + + {t('you_should_not_downgrade')} + + ) } diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/UpdateRobotSoftware.test.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/UpdateRobotSoftware.test.tsx index 4b5e2191ab7..1564a65d80d 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/UpdateRobotSoftware.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/UpdateRobotSoftware.test.tsx @@ -66,4 +66,13 @@ describe('RobotSettings UpdateRobotSoftware', () => { const button = screen.getByText('Browse file system') expect(button).toBeDisabled() }) + + it('should render a banner warning users about downgrading their robot', () => { + render() + screen.getByTestId('Banner_warning') + screen.getByLabelText('icon_warning') + screen.getByText( + 'You should not downgrade to a software version released before the manufacture date of your robot or any attached hardware.' + ) + }) }) diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx index 9ae9afdcee4..601967eb602 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -220,14 +220,15 @@ export function RobotSettingsAdvanced({ handleUpdateBuildroot(robot) }} /> + {isFlex ? ( <> - + ) : null} diff --git a/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx b/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx index 8abe226ead3..cbffbc34bb8 100644 --- a/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx @@ -1,21 +1,22 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' -import '@testing-library/jest-dom/vitest' -import { renderWithProviders } from '../../../__testing-utils__' import { when } from 'vitest-when' import { MemoryRouter } from 'react-router-dom' + import { useDeleteRunMutation } from '@opentrons/react-api-client' + +import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' -import runRecord from '../../../organisms/RunDetails/__fixtures__/runRecord.json' +import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' +import runRecord from '../ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__fixtures__/runRecord.json' import { useDownloadRunLog, useTrackProtocolRunEvent, useRobot } from '../hooks' import { useRunControls } from '../../RunTimeControl/hooks' import { useTrackEvent, ANALYTICS_PROTOCOL_PROCEED_TO_RUN, } from '../../../redux/analytics' -import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' -import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../../redux/robot-update' import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsEstopNotDisengaged' import { HistoricalProtocolRunOverflowMenu } from '../HistoricalProtocolRunOverflowMenu' import { useNotifyAllCommandsQuery } from '../../../resources/runs' @@ -31,6 +32,7 @@ vi.mock('../../../redux/analytics') vi.mock('../../../redux/config') vi.mock('../../../resources/devices/hooks/useIsEstopNotDisengaged') vi.mock('../../../resources/runs') +vi.mock('../../../redux/robot-update') vi.mock('@opentrons/react-api-client') const render = ( @@ -58,11 +60,7 @@ describe('HistoricalProtocolRunOverflowMenu', () => { mockTrackEvent = vi.fn() vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) mockTrackProtocolRunEvent = vi.fn(() => new Promise(resolve => resolve({}))) - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'reinstall', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(false) vi.mocked(useDownloadRunLog).mockReturnValue({ downloadRunLog: mockDownloadRunLog, isRunLogLoading: false, @@ -87,6 +85,7 @@ describe('HistoricalProtocolRunOverflowMenu', () => { isStopRunActionLoading: false, isResetRunLoading: false, isResumeRunFromRecoveryActionLoading: false, + isRunControlLoading: false, }) when(useNotifyAllCommandsQuery) .calledWith( @@ -139,11 +138,33 @@ describe('HistoricalProtocolRunOverflowMenu', () => { }) it('disables the rerun protocol menu item if robot software update is available', () => { - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(true) + render(props) + const btn = screen.getByRole('button') + fireEvent.click(btn) + screen.getByRole('button', { + name: 'View protocol run record', }) + const rerunBtn = screen.getByRole('button', { name: 'Rerun protocol now' }) + expect(rerunBtn).toBeDisabled() + }) + + it('disables the rerun protocol menu item if run data is loading', () => { + when(useRunControls) + .calledWith(RUN_ID, expect.anything()) + .thenReturn({ + play: () => {}, + pause: () => {}, + stop: () => {}, + reset: () => {}, + resumeFromRecovery: () => {}, + isPlayRunActionLoading: false, + isPauseRunActionLoading: false, + isStopRunActionLoading: false, + isResetRunLoading: false, + isResumeRunFromRecoveryActionLoading: false, + isRunControlLoading: true, + }) render(props) const btn = screen.getByRole('button') fireEvent.click(btn) diff --git a/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx b/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx index 868e14cf171..f79a1885d40 100644 --- a/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx @@ -8,7 +8,7 @@ import { i18n } from '../../../i18n' import { useCurrentRunId } from '../../../resources/runs' import { ChooseProtocolSlideout } from '../../ChooseProtocolSlideout' import { RobotOverflowMenu } from '../RobotOverflowMenu' -import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../../redux/robot-update' import { useIsRobotBusy } from '../hooks' import { @@ -16,7 +16,7 @@ import { mockConnectedRobot, } from '../../../redux/discovery/__fixtures__' -vi.mock('../../../redux/robot-update/selectors') +vi.mock('../../../redux/robot-update/hooks') vi.mock('../../../resources/runs') vi.mock('../../ChooseProtocolSlideout') vi.mock('../hooks') @@ -44,11 +44,7 @@ describe('RobotOverflowMenu', () => { vi.mocked(ChooseProtocolSlideout).mockReturnValue(
choose protocol slideout
) - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'reinstall', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(false) vi.mocked(useIsRobotBusy).mockReturnValue(false) }) @@ -74,11 +70,7 @@ describe('RobotOverflowMenu', () => { it('disables the run a protocol menu item if robot software update is available', () => { vi.mocked(useCurrentRunId).mockReturnValue(null) - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(true) render(props) const btn = screen.getByLabelText('RobotOverflowMenu_button') fireEvent.click(btn) diff --git a/app/src/organisms/Devices/__tests__/RobotOverviewOverflowMenu.test.tsx b/app/src/organisms/Devices/__tests__/RobotOverviewOverflowMenu.test.tsx index 2cbcab8b99c..b60cd6e147a 100644 --- a/app/src/organisms/Devices/__tests__/RobotOverviewOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotOverviewOverflowMenu.test.tsx @@ -8,7 +8,7 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { home } from '../../../redux/robot-controls' -import * as Buildroot from '../../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../../redux/robot-update' import { restartRobot } from '../../../redux/robot-admin' import { mockConnectableRobot, @@ -24,8 +24,6 @@ import { handleUpdateBuildroot } from '../RobotSettings/UpdateBuildroot' import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsEstopNotDisengaged' import { RobotOverviewOverflowMenu } from '../RobotOverviewOverflowMenu' -import type { State } from '../../../redux/types' - vi.mock('../../../redux/robot-controls') vi.mock('../../../redux/robot-admin') vi.mock('../hooks') @@ -39,8 +37,6 @@ vi.mock('../../../resources/runs') vi.mock('../RobotSettings/UpdateBuildroot') vi.mock('../../../resources/devices/hooks/useIsEstopNotDisengaged') -const getBuildrootUpdateDisplayInfo = Buildroot.getRobotUpdateDisplayInfo - const render = ( props: React.ComponentProps ) => { @@ -60,13 +56,7 @@ describe('RobotOverviewOverflowMenu', () => { beforeEach(() => { props = { robot: mockConnectableRobot } - when(getBuildrootUpdateDisplayInfo) - .calledWith({} as State, mockConnectableRobot.name) - .thenReturn({ - autoUpdateAction: 'reinstall', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(false) vi.mocked(useCurrentRunId).mockReturnValue(null) vi.mocked(useIsRobotBusy).mockReturnValue(false) vi.mocked(handleUpdateBuildroot).mockReturnValue() @@ -107,13 +97,7 @@ describe('RobotOverviewOverflowMenu', () => { }) it('should render update robot software button when robot is on wrong version of software', () => { - when(getBuildrootUpdateDisplayInfo) - .calledWith({} as State, mockConnectableRobot.name) - .thenReturn({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(true) render(props) @@ -238,11 +222,6 @@ describe('RobotOverviewOverflowMenu', () => { expect(restartRobot).toBeCalled() }) it('render overflow menu buttons without the update robot software button', () => { - vi.mocked(getBuildrootUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'reinstall', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) render(props) const btn = screen.getByRole('button') fireEvent.click(btn) @@ -263,11 +242,6 @@ describe('RobotOverviewOverflowMenu', () => { }) it('should render disabled menu items except restart robot and robot settings when e-stop is pressed', () => { - vi.mocked(getBuildrootUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'reinstall', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) when(useIsEstopNotDisengaged) .calledWith(mockConnectableRobot.name) .thenReturn(true) diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx index 45c2546efd0..02e3548c915 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx @@ -58,6 +58,7 @@ describe('useProtocolDetailsForRun hook', () => { protocolKey: null, isProtocolAnalyzing: false, robotType: 'OT-3 Standard', + isQuickTransfer: false, }) }) @@ -95,6 +96,7 @@ describe('useProtocolDetailsForRun hook', () => { protocolKey: 'fakeProtocolKey', isProtocolAnalyzing: false, robotType: 'OT-2 Standard', + isQuickTransfer: false, }) }) }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useRunStatuses.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useRunStatuses.test.tsx index 9277ddafd10..db738d4cafd 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useRunStatuses.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useRunStatuses.test.tsx @@ -5,8 +5,14 @@ import { RUN_STATUS_RUNNING, RUN_STATUS_STOPPED, RUN_STATUS_SUCCEEDED, + RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, + RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_FINISHING, + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, } from '@opentrons/api-client' -import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' +import { vi, it, expect, describe, beforeEach } from 'vitest' import { useCurrentRunId } from '../../../../resources/runs' import { useRunStatus } from '../../../RunTimeControl/hooks' @@ -15,14 +21,11 @@ import { useRunStatuses } from '..' vi.mock('../../../../resources/runs') vi.mock('../../../RunTimeControl/hooks') -describe(' useRunStatuses ', () => { +describe('useRunStatuses', () => { beforeEach(() => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_RUNNING) vi.mocked(useCurrentRunId).mockReturnValue('123') }) - afterEach(() => { - vi.resetAllMocks() - }) it('returns everything as false when run status is null', () => { vi.mocked(useRunStatus).mockReturnValue(null) @@ -35,7 +38,7 @@ describe(' useRunStatuses ', () => { }) }) - it('returns true isRunStill and Terminal when run status is suceeded', () => { + it(`returns true isRunStill and Terminal when run status is ${RUN_STATUS_SUCCEEDED}`, () => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_SUCCEEDED) const result = useRunStatuses() expect(result).toStrictEqual({ @@ -46,7 +49,7 @@ describe(' useRunStatuses ', () => { }) }) - it('returns true isRunStill and Terminal when run status is stopped', () => { + it(`returns true isRunStill and Terminal when run status is ${RUN_STATUS_STOPPED}`, () => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_STOPPED) const result = useRunStatuses() expect(result).toStrictEqual({ @@ -57,7 +60,7 @@ describe(' useRunStatuses ', () => { }) }) - it('returns true isRunStill and Terminal when run status is failed', () => { + it(`returns true isRunStill and Terminal when run status is ${RUN_STATUS_FAILED}`, () => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_FAILED) const result = useRunStatuses() expect(result).toStrictEqual({ @@ -68,7 +71,7 @@ describe(' useRunStatuses ', () => { }) }) - it('returns true isRunStill and isRunIdle when run status is idle', () => { + it(`returns true isRunStill and isRunIdle when run status is ${RUN_STATUS_IDLE}`, () => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_IDLE) const result = useRunStatuses() expect(result).toStrictEqual({ @@ -79,7 +82,7 @@ describe(' useRunStatuses ', () => { }) }) - it('returns true isRunRunning when status is running', () => { + it(`returns true isRunRunning when status is ${RUN_STATUS_RUNNING}`, () => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_RUNNING) const result = useRunStatuses() expect(result).toStrictEqual({ @@ -90,7 +93,7 @@ describe(' useRunStatuses ', () => { }) }) - it('returns true isRunRunning when status is paused', () => { + it(`returns true isRunRunning when status is ${RUN_STATUS_PAUSED}`, () => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_PAUSED) const result = useRunStatuses() expect(result).toStrictEqual({ @@ -100,4 +103,72 @@ describe(' useRunStatuses ', () => { isRunIdle: false, }) }) + + it(`returns true isRunRunning when status is ${RUN_STATUS_AWAITING_RECOVERY}`, () => { + vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_AWAITING_RECOVERY) + const result = useRunStatuses() + expect(result).toStrictEqual({ + isRunRunning: true, + isRunStill: false, + isRunTerminal: false, + isRunIdle: false, + }) + }) + + it(`returns true isRunRunning when status is ${RUN_STATUS_AWAITING_RECOVERY_PAUSED}`, () => { + vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_AWAITING_RECOVERY_PAUSED) + const result = useRunStatuses() + expect(result).toStrictEqual({ + isRunRunning: true, + isRunStill: false, + isRunTerminal: false, + isRunIdle: false, + }) + }) + + it(`returns true isRunRunning when status is ${RUN_STATUS_STOP_REQUESTED}`, () => { + vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_STOP_REQUESTED) + const result = useRunStatuses() + expect(result).toStrictEqual({ + isRunRunning: true, + isRunStill: false, + isRunTerminal: false, + isRunIdle: false, + }) + }) + + it(`returns true isRunRunning when status is ${RUN_STATUS_FINISHING}`, () => { + vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_FINISHING) + const result = useRunStatuses() + expect(result).toStrictEqual({ + isRunRunning: true, + isRunStill: false, + isRunTerminal: false, + isRunIdle: false, + }) + }) + + it(`returns true isRunRunning when status is ${RUN_STATUS_BLOCKED_BY_OPEN_DOOR}`, () => { + vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_BLOCKED_BY_OPEN_DOOR) + const result = useRunStatuses() + expect(result).toStrictEqual({ + isRunRunning: true, + isRunStill: false, + isRunTerminal: false, + isRunIdle: false, + }) + }) + + it(`returns true isRunRunning when status is ${RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR}`, () => { + vi.mocked(useRunStatus).mockReturnValue( + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR + ) + const result = useRunStatuses() + expect(result).toStrictEqual({ + isRunRunning: true, + isRunStill: false, + isRunTerminal: false, + isRunIdle: false, + }) + }) }) diff --git a/app/src/organisms/Devices/hooks/useCalibrationTaskList.ts b/app/src/organisms/Devices/hooks/useCalibrationTaskList.ts index 6cd2e75c4a9..f46383b4007 100644 --- a/app/src/organisms/Devices/hooks/useCalibrationTaskList.ts +++ b/app/src/organisms/Devices/hooks/useCalibrationTaskList.ts @@ -19,9 +19,9 @@ import type { TaskProps, } from '../../TaskList/types' import type { AttachedPipette } from '../../../redux/pipettes/types' -import type { DashboardCalOffsetInvoker } from '../../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset' -import type { DashboardCalTipLengthInvoker } from '../../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' -import type { DashboardCalDeckInvoker } from '../../../pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck' +import type { DashboardCalOffsetInvoker } from '../../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset' +import type { DashboardCalTipLengthInvoker } from '../../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength' +import type { DashboardCalDeckInvoker } from '../../../pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck' const CALIBRATION_DATA_POLL_MS = 5000 diff --git a/app/src/organisms/Devices/hooks/useDownloadRunLog.ts b/app/src/organisms/Devices/hooks/useDownloadRunLog.ts index 1652efc4442..777f50bb806 100644 --- a/app/src/organisms/Devices/hooks/useDownloadRunLog.ts +++ b/app/src/organisms/Devices/hooks/useDownloadRunLog.ts @@ -29,12 +29,14 @@ export function useDownloadRunLog( getCommands(host, runId, { cursor: null, pageLength: 0, + includeFixitCommands: true, }) .then(response => { const { totalLength } = response.data.meta getCommands(host, runId, { cursor: 0, pageLength: totalLength, + includeFixitCommands: true, }) .then(response => { const commands = response.data diff --git a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts index 57c50666488..13a00225383 100644 --- a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts +++ b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts @@ -21,6 +21,7 @@ export interface ProtocolDetails { protocolKey: string | null isProtocolAnalyzing?: boolean robotType: RobotType + isQuickTransfer: boolean } export function useProtocolDetailsForRun( @@ -67,5 +68,6 @@ export function useProtocolDetailsForRun( (mostRecentAnalysis?.status === 'completed' ? mostRecentAnalysis?.robotType ?? FLEX_ROBOT_TYPE : FLEX_ROBOT_TYPE), + isQuickTransfer: protocolRecord?.data.protocolKind === 'quick-transfer', } } diff --git a/app/src/organisms/Devices/hooks/useRunStatuses.ts b/app/src/organisms/Devices/hooks/useRunStatuses.ts index bf1c550efa0..39c15251c7a 100644 --- a/app/src/organisms/Devices/hooks/useRunStatuses.ts +++ b/app/src/organisms/Devices/hooks/useRunStatuses.ts @@ -5,6 +5,10 @@ import { RUN_STATUS_IDLE, RUN_STATUS_PAUSED, RUN_STATUS_RUNNING, + RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_FINISHING, + RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, } from '@opentrons/api-client' import { useCurrentRunId } from '../../../resources/runs' import { useRunStatus } from '../../RunTimeControl/hooks' @@ -23,16 +27,14 @@ export function useRunStatuses(): RunStatusesInfo { const runStatus = useRunStatus(currentRunId) const isRunIdle = runStatus === RUN_STATUS_IDLE const isRunRunning = - // todo(mm, 2024-03-13): This excludes statuses like: - // * RUN_STATUS_FINISHING - // * RUN_STATUS_STOP_REQUESTED - // * RUN_STATUS_BLOCKED_BY_OPEN_DOOR - // * RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR - // And it's not clear whether that's intentional. runStatus === RUN_STATUS_PAUSED || runStatus === RUN_STATUS_RUNNING || runStatus === RUN_STATUS_AWAITING_RECOVERY || - runStatus === RUN_STATUS_AWAITING_RECOVERY_PAUSED + runStatus === RUN_STATUS_AWAITING_RECOVERY_PAUSED || + runStatus === RUN_STATUS_STOP_REQUESTED || + runStatus === RUN_STATUS_FINISHING || + runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR || + runStatus === RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR const isRunTerminal = runStatus != null ? (RUN_STATUSES_TERMINAL as RunStatus[]).includes(runStatus) diff --git a/app/src/organisms/DropTipWizardFlows/DropTipWizardFlows.tsx b/app/src/organisms/DropTipWizardFlows/DropTipWizardFlows.tsx new file mode 100644 index 00000000000..555ba854c36 --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/DropTipWizardFlows.tsx @@ -0,0 +1,61 @@ +import * as React from 'react' + +import { useDropTipRouting, useDropTipWithType } from './hooks' +import { DropTipWizard } from './DropTipWizard' + +import type { PipetteModelSpecs, RobotType } from '@opentrons/shared-data' +import type { PipetteData } from '@opentrons/api-client' +import type { FixitCommandTypeUtils, IssuedCommandsType } from './types' + +/** Provides the user toggle for rendering Drop Tip Wizard Flows. + * + * NOTE: Rendering these flows is independent of whether tips are actually attached. First use useTipAttachmentStatus + * to get tip attachment status. + */ +export function useDropTipWizardFlows(): { + showDTWiz: boolean + toggleDTWiz: () => void +} { + const [showDTWiz, setShowDTWiz] = React.useState(false) + + const toggleDTWiz = (): void => { + setShowDTWiz(!showDTWiz) + } + + return { showDTWiz, toggleDTWiz } +} + +export interface DropTipWizardFlowsProps { + robotType: RobotType + mount: PipetteData['mount'] + instrumentModelSpecs: PipetteModelSpecs + /* isTakeover allows for optionally specifying a different callback if a different client cancels the "setup" type flow. */ + closeFlow: (isTakeover?: boolean) => void + /* Optional. If provided, DT will issue "fixit" commands and render alternate Error Recovery compatible views. */ + fixitCommandTypeUtils?: FixitCommandTypeUtils +} + +export function DropTipWizardFlows( + props: DropTipWizardFlowsProps +): JSX.Element { + const { fixitCommandTypeUtils } = props + + const issuedCommandsType: IssuedCommandsType = + fixitCommandTypeUtils != null ? 'fixit' : 'setup' + + const dropTipWithTypeUtils = useDropTipWithType({ + ...props, + issuedCommandsType, + }) + + const dropTipRoutingUtils = useDropTipRouting(fixitCommandTypeUtils) + + return ( + + ) +} diff --git a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx index 7c9c5a11823..36a50f8e47f 100644 --- a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx +++ b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx @@ -19,17 +19,13 @@ import { useHomePipettes } from './hooks' import type { HostConfig } from '@opentrons/api-client' import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' -import type { PipetteWithTip } from '.' -import type { UseHomePipettesProps } from './hooks' +import type { UseHomePipettesProps, PipetteWithTip } from './hooks' +import type { PipetteDetails } from '../../resources/maintenance_runs' -type TipsAttachedModalProps = Pick< - UseHomePipettesProps, - 'robotType' | 'instrumentModelSpecs' | 'mount' | 'isRunCurrent' -> & { +type TipsAttachedModalProps = Pick & { aPipetteWithTip: PipetteWithTip host: HostConfig | null setTipStatusResolved: (onEmpty?: () => void) => Promise - onSkipAndHome: () => void } export const handleTipsAttachedModal = ( @@ -53,9 +49,10 @@ const TipsAttachedModal = NiceModal.create( const { mount, specs } = aPipetteWithTip const { showDTWiz, toggleDTWiz } = useDropTipWizardFlows() - const { homePipettes, isHomingPipettes } = useHomePipettes({ + const { homePipettes, isHoming } = useHomePipettes({ ...homePipetteProps, - onHome: () => { + pipetteInfo: buildPipetteDetails(aPipetteWithTip), + onSettled: () => { modal.remove() void setTipStatusResolved() }, @@ -105,13 +102,13 @@ const TipsAttachedModal = NiceModal.create( buttonType="secondary" buttonText={t('skip_and_home_pipette')} onClick={onHomePipettes} - disabled={isHomingPipettes} + disabled={isHoming} /> @@ -130,3 +127,15 @@ const TipsAttachedModal = NiceModal.create( ) } ) + +// TODO(jh, 09-12-24): Consolidate this with the same utility that exists elsewhere. +function buildPipetteDetails( + aPipetteWithTip: PipetteWithTip | null +): PipetteDetails | null { + return aPipetteWithTip != null + ? { + pipetteId: aPipetteWithTip.specs.name, + mount: aPipetteWithTip.mount, + } + : null +} diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardFlows.test.ts b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardFlows.test.ts new file mode 100644 index 00000000000..bf85054259a --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardFlows.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect, vi } from 'vitest' +import { renderHook, act } from '@testing-library/react' + +import { useDropTipWizardFlows } from '..' + +vi.mock('../DropTipWizard') +vi.mock('../hooks') + +describe('useDropTipWizardFlows', () => { + it('should toggle showDTWiz state', () => { + const { result } = renderHook(() => useDropTipWizardFlows()) + + expect(result.current.showDTWiz).toBe(false) + + act(() => { + result.current.toggleDTWiz() + }) + + expect(result.current.showDTWiz).toBe(true) + }) +}) diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx b/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx index 40a2b075e7c..8dd0251038b 100644 --- a/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx @@ -7,15 +7,15 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { handleTipsAttachedModal } from '../TipsAttachedModal' -import { FLEX_ROBOT_TYPE, LEFT } from '@opentrons/shared-data' +import { LEFT } from '@opentrons/shared-data' import { mockPipetteInfo } from '../../../redux/pipettes/__fixtures__' import { useCloseCurrentRun } from '../../ProtocolUpload/hooks' import { useDropTipWizardFlows } from '..' +import type { Mock } from 'vitest' import type { PipetteModelSpecs } from '@opentrons/shared-data' import type { HostConfig } from '@opentrons/api-client' -import type { Mock } from 'vitest' -import type { PipetteWithTip } from '..' +import type { PipetteWithTip } from '../hooks' vi.mock('../../ProtocolUpload/hooks') vi.mock('..') @@ -52,11 +52,7 @@ const render = (aPipetteWithTip: PipetteWithTip) => { host: MOCK_HOST, aPipetteWithTip, setTipStatusResolved: mockSetTipStatusResolved, - robotType: FLEX_ROBOT_TYPE, - mount: 'left', - instrumentModelSpecs: mockPipetteInfo.pipetteSpecs as any, - onSkipAndHome: vi.fn(), - isRunCurrent: true, + onSettled: vi.fn(), }) } data-testid="testButton" diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardFlows.test.tsx b/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx similarity index 71% rename from app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardFlows.test.tsx rename to app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx index 99d08eaa579..208caefb4dc 100644 --- a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardFlows.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx @@ -1,23 +1,18 @@ import * as React from 'react' -import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' -import { screen, renderHook, act } from '@testing-library/react' - -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { mockPipetteInfo } from '../../../redux/pipettes/__fixtures__' -import { - useTipAttachmentStatus, - useDropTipWizardFlows, - DropTipWizardFlows, -} from '..' -import { getPipettesWithTipAttached } from '../getPipettesWithTipAttached' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { act, renderHook } from '@testing-library/react' + import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { DropTipWizard } from '../DropTipWizard' import { useInstrumentsQuery } from '@opentrons/react-api-client' +import { mockPipetteInfo } from '../../../../redux/pipettes/__fixtures__' +import { getPipettesWithTipAttached } from '../useTipAttachmentStatus/getPipettesWithTipAttached' +import { DropTipWizard } from '../../DropTipWizard' +import { useTipAttachmentStatus } from '../useTipAttachmentStatus' + import type { Mock } from 'vitest' import type { PipetteModelSpecs } from '@opentrons/shared-data' -import type { PipetteWithTip } from '..' +import type { PipetteWithTip } from '../useTipAttachmentStatus' vi.mock('@opentrons/shared-data', async importOriginal => { const actual = await importOriginal() @@ -26,10 +21,9 @@ vi.mock('@opentrons/shared-data', async importOriginal => { getPipetteModelSpecs: vi.fn(), } }) -vi.mock('../DropTipWizard') -vi.mock('../getPipettesWithTipAttached') -vi.mock('../hooks') vi.mock('@opentrons/react-api-client') +vi.mock('../useTipAttachmentStatus/getPipettesWithTipAttached') +vi.mock('../../DropTipWizard') const MOCK_ACTUAL_PIPETTE = { ...mockPipetteInfo.pipetteSpecs, @@ -124,31 +118,3 @@ describe('useTipAttachmentStatus', () => { expect(onEmptyCacheMock).toHaveBeenCalled() }) }) - -describe('useDropTipWizardFlows', () => { - it('should toggle showDTWiz state', () => { - const { result } = renderHook(() => useDropTipWizardFlows()) - - expect(result.current.showDTWiz).toBe(false) - - act(() => { - result.current.toggleDTWiz() - }) - - expect(result.current.showDTWiz).toBe(true) - }) -}) - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('DropTipWizardFlows', () => { - it('should render DropTipWizard', () => { - render({} as any) - - screen.getByText('MOCK DROP TIP WIZ') - }) -}) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/index.ts b/app/src/organisms/DropTipWizardFlows/hooks/index.ts index fdb5964eace..890578280cc 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/index.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/index.ts @@ -1,6 +1,7 @@ export * from './errors' export * from './useDropTipWithType' export * from './useHomePipettes' +export * from './useTipAttachmentStatus' export { useDropTipRouting } from './useDropTipRouting' export { useDropTipWithType } from './useDropTipWithType' diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts index 78e905ae5e1..5c073588ee8 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts @@ -3,7 +3,7 @@ import * as React from 'react' import { useDeleteMaintenanceRunMutation } from '@opentrons/react-api-client' import { MANAGED_PIPETTE_ID, POSITION_AND_BLOWOUT } from '../constants' -import { getAddressableAreaFromConfig } from '../getAddressableAreaFromConfig' +import { getAddressableAreaFromConfig } from '../utils' import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' import type { CreateCommand, @@ -33,7 +33,6 @@ type UseDropTipSetupCommandsParams = UseDTWithTypeParams & { setErrorDetails: (errorDetails: SetRobotErrorDetailsParams) => void toggleIsExiting: () => void fixitCommandTypeUtils?: FixitCommandTypeUtils - toggleClientEndRun: () => void } export interface UseDropTipCommandsResult { @@ -58,7 +57,6 @@ export function useDropTipCommands({ instrumentModelSpecs, robotType, fixitCommandTypeUtils, - toggleClientEndRun, }: UseDropTipSetupCommandsParams): UseDropTipCommandsResult { const isFlex = robotType === FLEX_ROBOT_TYPE const [hasSeenClose, setHasSeenClose] = React.useState(false) @@ -89,7 +87,6 @@ export function useDropTipCommands({ console.error(error.message) }) .finally(() => { - toggleClientEndRun() closeFlow() deleteMaintenanceRun(activeMaintenanceRunId) }) @@ -117,7 +114,7 @@ export function useDropTipCommands({ ) return chainRunCommands( isFlex - ? [UPDATE_ESTIMATORS_EXCEPT_PLUNGERS, moveToAACommand] + ? [ENGAGE_AXES, UPDATE_ESTIMATORS_EXCEPT_PLUNGERS, moveToAACommand] : [moveToAACommand], true ) @@ -288,6 +285,13 @@ const HOME: CreateCommand = { params: {}, } +const ENGAGE_AXES: CreateCommand = { + commandType: 'unsafe/engageAxes' as const, + params: { + axes: ['leftZ', 'rightZ', 'x', 'y', 'leftPlunger', 'rightPlunger'], + }, +} + const HOME_EXCEPT_PLUNGERS: CreateCommand = { commandType: 'home' as const, params: { axes: ['leftZ', 'rightZ', 'x', 'y'] }, @@ -298,6 +302,11 @@ const UPDATE_ESTIMATORS_EXCEPT_PLUNGERS: CreateCommand = { params: { axes: ['leftZ', 'rightZ', 'x', 'y'] }, } +const UPDATE_PLUNGER_ESTIMATORS: CreateCommand = { + commandType: 'unsafe/updatePositionEstimators' as const, + params: { axes: ['leftPlunger', 'rightPlunger'] }, +} + const buildDropTipInPlaceCommand = ( isFlex: boolean, pipetteId: string | null @@ -323,6 +332,8 @@ const buildBlowoutCommands = ( ): CreateCommand[] => isFlex ? [ + ENGAGE_AXES, + UPDATE_PLUNGER_ESTIMATORS, { commandType: 'unsafe/blowOutInPlace', params: { @@ -342,6 +353,8 @@ const buildBlowoutCommands = ( }, ] : [ + ENGAGE_AXES, + UPDATE_PLUNGER_ESTIMATORS, { commandType: 'blowOutInPlace', params: { diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCreateCommands.ts b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCreateCommands.ts index 10112ea740b..5b84e3a81dd 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCreateCommands.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCreateCommands.ts @@ -1,7 +1,6 @@ import { useCreateMaintenanceCommandMutation } from '@opentrons/react-api-client' import { - useChainMaintenanceCommands, useChainRunCommands, useCreateRunCommandMutation, } from '../../../resources/runs' @@ -10,6 +9,7 @@ import type { CreateCommand } from '@opentrons/shared-data' import type { CommandData } from '@opentrons/api-client' import type { UseDTWithTypeParams, SetRobotErrorDetailsParams } from '.' import type { FixitCommandTypeUtils } from '../types' +import { useChainMaintenanceCommands } from '../../../resources/maintenance_runs' export interface RunCommandByCommandTypeParams { command: CreateCommand diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipMaintenanceRun.tsx b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipMaintenanceRun.tsx index 67f4fa7957a..64f3f05ec96 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipMaintenanceRun.tsx +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipMaintenanceRun.tsx @@ -1,10 +1,10 @@ import * as React from 'react' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import { - useCreateTargetedMaintenanceRunMutation, useChainMaintenanceCommands, -} from '../../../resources/runs' + useNotifyCurrentMaintenanceRun, +} from '../../../resources/maintenance_runs' +import { useCreateTargetedMaintenanceRunMutation } from '../../../resources/runs' import { buildLoadPipetteCommand } from './useDropTipCommands' import type { PipetteModelSpecs } from '@opentrons/shared-data' @@ -20,13 +20,8 @@ export type UseDropTipMaintenanceRunParams = Omit< setErrorDetails?: (errorDetails: SetRobotErrorDetailsParams) => void instrumentModelSpecs?: PipetteModelSpecs mount?: PipetteData['mount'] - /* Optionally control when a drop tip maintenance run is created. */ - enabled?: boolean } -// TODO(jh, 08-08-24): useDropTipMaintenanceRun is a bit overloaded now that we are using it create maintenance runs -// on-the-fly for one-off commands outside of a run. Consider refactoring. - // Manages the maintenance run state if the flow is utilizing "setup" type commands. export function useDropTipMaintenanceRun({ issuedCommandsType, @@ -34,11 +29,7 @@ export function useDropTipMaintenanceRun({ instrumentModelSpecs, setErrorDetails, closeFlow, - enabled, -}: UseDropTipMaintenanceRunParams): { - activeMaintenanceRunId: string | null - toggleClientEndRun: () => void -} { +}: UseDropTipMaintenanceRunParams): string | null { const isMaintenanceRunType = issuedCommandsType === 'setup' const [createdMaintenanceRunId, setCreatedMaintenanceRunId] = React.useState< @@ -57,20 +48,16 @@ export function useDropTipMaintenanceRun({ instrumentModelName: instrumentModelSpecs?.name, setErrorDetails, setCreatedMaintenanceRunId, - enabled, }) - const toggleClientEndRun = useMonitorMaintenanceRunForDeletion({ + useMonitorMaintenanceRunForDeletion({ isMaintenanceRunType, activeMaintenanceRunId, createdMaintenanceRunId, closeFlow, }) - return { - activeMaintenanceRunId: activeMaintenanceRunId ?? null, - toggleClientEndRun, - } + return activeMaintenanceRunId ?? null } type UseCreateDropTipMaintenanceRunParams = Omit< @@ -88,7 +75,6 @@ function useCreateDropTipMaintenanceRun({ instrumentModelName, setErrorDetails, setCreatedMaintenanceRunId, - enabled, }: UseCreateDropTipMaintenanceRunParams): void { const { chainRunCommands } = useChainMaintenanceCommands() @@ -115,13 +101,11 @@ function useCreateDropTipMaintenanceRun({ }, }) - const isEnabled = enabled ?? true React.useEffect(() => { if ( issuedCommandsType === 'setup' && mount != null && - instrumentModelName != null && - isEnabled + instrumentModelName != null ) { createTargetedMaintenanceRun({}).catch((e: Error) => { if (setErrorDetails != null) { @@ -131,18 +115,16 @@ function useCreateDropTipMaintenanceRun({ } }) } else { - if (mount != null || instrumentModelName != null) { - console.warn( - 'Could not create maintenance run due to missing pipette data.' - ) - } + console.warn( + 'Could not create maintenance run due to missing pipette data.' + ) } - }, [enabled, mount, instrumentModelName]) + }, [mount, instrumentModelName]) } interface UseMonitorMaintenanceRunForDeletionParams { isMaintenanceRunType: boolean - closeFlow: (isTakeover?: boolean) => void + closeFlow: () => void createdMaintenanceRunId: string | null activeMaintenanceRunId?: string } @@ -154,15 +136,12 @@ function useMonitorMaintenanceRunForDeletion({ createdMaintenanceRunId, activeMaintenanceRunId, closeFlow, -}: UseMonitorMaintenanceRunForDeletionParams): () => void { +}: UseMonitorMaintenanceRunForDeletionParams): void { const [ monitorMaintenanceRunForDeletion, setMonitorMaintenanceRunForDeletion, ] = React.useState(false) const [closedOnce, setClosedOnce] = React.useState(false) - const [closedByThisClient, setClosedByThisClient] = React.useState( - false - ) React.useEffect(() => { if (isMaintenanceRunType && !closedOnce) { @@ -174,16 +153,11 @@ function useMonitorMaintenanceRunForDeletion({ } if ( activeMaintenanceRunId !== createdMaintenanceRunId && - monitorMaintenanceRunForDeletion && - !closedByThisClient + monitorMaintenanceRunForDeletion ) { - closeFlow(true) + closeFlow() setClosedOnce(true) } } }, [isMaintenanceRunType, createdMaintenanceRunId, activeMaintenanceRunId]) - - return () => { - setClosedByThisClient(!closedByThisClient) - } } diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipWithType.ts b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipWithType.ts index 63b3ab05da3..cb7064ac722 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipWithType.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipWithType.ts @@ -41,10 +41,7 @@ export function useDropTipWithType( const { isExiting, toggleIsExiting } = useIsExitingDT(issuedCommandsType) const { errorDetails, setErrorDetails } = useErrorDetails() - const { - activeMaintenanceRunId, - toggleClientEndRun, - } = useDropTipMaintenanceRun({ + const activeMaintenanceRunId = useDropTipMaintenanceRun({ ...params, setErrorDetails, }) @@ -63,7 +60,6 @@ export function useDropTipWithType( setErrorDetails, toggleIsExiting, fixitCommandTypeUtils, - toggleClientEndRun, }) useRegisterPipetteFixitType({ ...params, ...dtCreateCommandUtils }) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useHomePipettes.ts b/app/src/organisms/DropTipWizardFlows/hooks/useHomePipettes.ts index cb3576c7105..2a828dcfedf 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useHomePipettes.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/useHomePipettes.ts @@ -1,76 +1,34 @@ -import * as React from 'react' +import { useRobotControlCommands } from '../../../resources/maintenance_runs' -import { - useCreateMaintenanceCommandMutation, - useDeleteMaintenanceRunMutation, -} from '@opentrons/react-api-client' - -import { useDropTipMaintenanceRun } from './useDropTipMaintenanceRun' - -import type { UseDropTipMaintenanceRunParams } from './useDropTipMaintenanceRun' import type { CreateCommand } from '@opentrons/shared-data' - -export type UseHomePipettesProps = Omit< - UseDropTipMaintenanceRunParams, - 'issuedCommandsType' | 'closeFlow' -> & { - onHome: () => void - isRunCurrent: boolean +import type { + UseRobotControlCommandsProps, + UseRobotControlCommandsResult, +} from '../../../resources/maintenance_runs' + +interface UseHomePipettesResult { + isHoming: UseRobotControlCommandsResult['isExecuting'] + homePipettes: UseRobotControlCommandsResult['executeCommands'] } +export type UseHomePipettesProps = Pick< + UseRobotControlCommandsProps, + 'pipetteInfo' | 'onSettled' +> +// TODO(jh, 09-12-24): Find a better place for this hook to live. +// Home pipettes except for plungers. export function useHomePipettes( props: UseHomePipettesProps -): { - homePipettes: () => void - isHomingPipettes: boolean -} { - const [isHomingPipettes, setIsHomingPipettes] = React.useState(false) - const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation() - - const { activeMaintenanceRunId } = useDropTipMaintenanceRun({ +): UseHomePipettesResult { + const { executeCommands, isExecuting } = useRobotControlCommands({ ...props, - issuedCommandsType: 'setup', - enabled: isHomingPipettes, - closeFlow: props.onHome, + commands: [HOME_EXCEPT_PLUNGERS], + continuePastCommandFailure: true, }) - const isMaintenanceRunActive = activeMaintenanceRunId != null - - // Home the pipette after user click once a maintenance run has been created. - React.useEffect(() => { - if (isMaintenanceRunActive && isHomingPipettes && props.isRunCurrent) { - void homePipettesCmd().finally(() => { - props.onHome() - deleteMaintenanceRun(activeMaintenanceRunId) - }) - } - }, [isMaintenanceRunActive, isHomingPipettes, props.isRunCurrent]) - - const { createMaintenanceCommand } = useCreateMaintenanceCommandMutation() - - const homePipettesCmd = React.useCallback(() => { - if (activeMaintenanceRunId != null) { - return createMaintenanceCommand( - { - maintenanceRunId: activeMaintenanceRunId, - command: HOME_EXCEPT_PLUNGERS, - waitUntilComplete: true, - }, - { onSettled: () => Promise.resolve() } - ) - } else { - return Promise.reject( - new Error( - "'Unable to create a maintenance run when attempting to home pipettes." - ) - ) - } - }, [createMaintenanceCommand, activeMaintenanceRunId]) return { - homePipettes: () => { - setIsHomingPipettes(true) - }, - isHomingPipettes, + isHoming: isExecuting, + homePipettes: executeCommands, } } diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/getPipettesWithTipAttached.test.ts b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/__tests__/getPipettesWithTipAttached.test.ts similarity index 100% rename from app/src/organisms/DropTipWizardFlows/__tests__/getPipettesWithTipAttached.test.ts rename to app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/__tests__/getPipettesWithTipAttached.test.ts diff --git a/app/src/organisms/DropTipWizardFlows/getPipettesWithTipAttached.ts b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts similarity index 98% rename from app/src/organisms/DropTipWizardFlows/getPipettesWithTipAttached.ts rename to app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts index 99bcd949093..42b006ca0b2 100644 --- a/app/src/organisms/DropTipWizardFlows/getPipettesWithTipAttached.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts @@ -50,11 +50,13 @@ function getCommandsExecutedDuringRun( return getCommands(host, runId, { cursor: null, pageLength: 0, + includeFixitCommands: true, }).then(response => { const { totalLength } = response.data.meta return getCommands(host, runId, { cursor: 0, pageLength: totalLength, + includeFixitCommands: null, }).then(response => response.data) }) } diff --git a/app/src/organisms/DropTipWizardFlows/index.tsx b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts similarity index 69% rename from app/src/organisms/DropTipWizardFlows/index.tsx rename to app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts index f559aaa8e31..80d1bb1913c 100644 --- a/app/src/organisms/DropTipWizardFlows/index.tsx +++ b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts @@ -1,70 +1,14 @@ import * as React from 'react' import head from 'lodash/head' +import { useInstrumentsQuery } from '@opentrons/react-api-client' import { getPipetteModelSpecs } from '@opentrons/shared-data' import { getPipettesWithTipAttached } from './getPipettesWithTipAttached' -import { useDropTipRouting, useDropTipWithType } from './hooks' -import { DropTipWizard } from './DropTipWizard' -import type { PipetteModelSpecs, RobotType } from '@opentrons/shared-data' -import type { Mount, PipetteData } from '@opentrons/api-client' -import type { FixitCommandTypeUtils, IssuedCommandsType } from './types' +import type { Mount } from '@opentrons/api-client' +import type { PipetteModelSpecs } from '@opentrons/shared-data' import type { GetPipettesWithTipAttached } from './getPipettesWithTipAttached' -import { useInstrumentsQuery } from '@opentrons/react-api-client' - -/** Provides the user toggle for rendering Drop Tip Wizard Flows. - * - * NOTE: Rendering these flows is independent of whether tips are actually attached. First use useTipAttachmentStatus - * to get tip attachment status. - */ -export function useDropTipWizardFlows(): { - showDTWiz: boolean - toggleDTWiz: () => void -} { - const [showDTWiz, setShowDTWiz] = React.useState(false) - - const toggleDTWiz = (): void => { - setShowDTWiz(!showDTWiz) - } - - return { showDTWiz, toggleDTWiz } -} - -export interface DropTipWizardFlowsProps { - robotType: RobotType - mount: PipetteData['mount'] - instrumentModelSpecs: PipetteModelSpecs - /* isTakeover allows for optionally specifying a different callback if a different client cancels the "setup" type flow. */ - closeFlow: (isTakeover?: boolean) => void - /* Optional. If provided, DT will issue "fixit" commands and render alternate Error Recovery compatible views. */ - fixitCommandTypeUtils?: FixitCommandTypeUtils -} - -export function DropTipWizardFlows( - props: DropTipWizardFlowsProps -): JSX.Element { - const { fixitCommandTypeUtils } = props - - const issuedCommandsType: IssuedCommandsType = - fixitCommandTypeUtils != null ? 'fixit' : 'setup' - - const dropTipWithTypeUtils = useDropTipWithType({ - ...props, - issuedCommandsType, - }) - - const dropTipRoutingUtils = useDropTipRouting(fixitCommandTypeUtils) - - return ( - - ) -} const INSTRUMENTS_POLL_MS = 5000 diff --git a/app/src/organisms/DropTipWizardFlows/index.ts b/app/src/organisms/DropTipWizardFlows/index.ts new file mode 100644 index 00000000000..0030fa29a5a --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/index.ts @@ -0,0 +1,10 @@ +export * from './DropTipWizardFlows' +export { useTipAttachmentStatus, useHomePipettes } from './hooks' +export * from './TipsAttachedModal' + +export type { + UseHomePipettesProps, + TipAttachmentStatusResult, + PipetteWithTip, +} from './hooks' +export type { FixitCommandTypeUtils } from './types' diff --git a/app/src/organisms/DropTipWizardFlows/getAddressableAreaFromConfig.ts b/app/src/organisms/DropTipWizardFlows/utils/getAddressableAreaFromConfig.ts similarity index 100% rename from app/src/organisms/DropTipWizardFlows/getAddressableAreaFromConfig.ts rename to app/src/organisms/DropTipWizardFlows/utils/getAddressableAreaFromConfig.ts diff --git a/app/src/organisms/DropTipWizardFlows/utils/index.ts b/app/src/organisms/DropTipWizardFlows/utils/index.ts new file mode 100644 index 00000000000..742cff97db1 --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/utils/index.ts @@ -0,0 +1 @@ +export * from './getAddressableAreaFromConfig' diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index 0047f3c9dee..a63dd9765d3 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -42,6 +42,8 @@ interface EstopPressedModalProps { closeModal: () => void isDismissedModal?: boolean setIsDismissedModal?: (isDismissedModal: boolean) => void + isWaitingForLogicalDisengage: boolean + setShouldSeeLogicalDisengage: () => void } export function EstopPressedModal({ @@ -49,11 +51,18 @@ export function EstopPressedModal({ closeModal, isDismissedModal, setIsDismissedModal, + isWaitingForLogicalDisengage, + setShouldSeeLogicalDisengage, }: EstopPressedModalProps): JSX.Element { const isOnDevice = useSelector(getIsOnDevice) return createPortal( isOnDevice ? ( - + ) : ( <> {isDismissedModal === false ? ( @@ -61,6 +70,8 @@ export function EstopPressedModal({ isEngaged={isEngaged} closeModal={closeModal} setIsDismissedModal={setIsDismissedModal} + isWaitingForLogicalDisengage={isWaitingForLogicalDisengage} + setShouldSeeLogicalDisengage={setShouldSeeLogicalDisengage} /> ) : null} @@ -72,6 +83,8 @@ export function EstopPressedModal({ function TouchscreenModal({ isEngaged, closeModal, + isWaitingForLogicalDisengage, + setShouldSeeLogicalDisengage, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation(['device_settings', 'branded']) const [isResuming, setIsResuming] = React.useState(false) @@ -88,6 +101,7 @@ function TouchscreenModal({ const handleClick = (): void => { setIsResuming(true) acknowledgeEstopDisengage(null) + setShouldSeeLogicalDisengage() closeModal() } return ( @@ -116,10 +130,16 @@ function TouchscreenModal({ @@ -131,6 +151,8 @@ function DesktopModal({ isEngaged, closeModal, setIsDismissedModal, + isWaitingForLogicalDisengage, + setShouldSeeLogicalDisengage, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation('device_settings') const [isResuming, setIsResuming] = React.useState(false) @@ -155,14 +177,19 @@ function DesktopModal({ const handleClick: React.MouseEventHandler = (e): void => { e.preventDefault() setIsResuming(true) - acknowledgeEstopDisengage({ - onSuccess: () => { - closeModal() - }, - onError: () => { - setIsResuming(false) - }, - }) + acknowledgeEstopDisengage( + {}, + { + onSuccess: () => { + setShouldSeeLogicalDisengage() + closeModal() + }, + onError: (error: any) => { + setIsResuming(false) + console.error(error) + }, + } + ) } return ( @@ -177,14 +204,16 @@ function DesktopModal({ - {isResuming ? : null} + {isResuming || isWaitingForLogicalDisengage ? ( + + ) : null} {t('resume_robot_operations')} diff --git a/app/src/organisms/EmergencyStop/EstopTakeover.tsx b/app/src/organisms/EmergencyStop/EstopTakeover.tsx index 7c3b07ce062..4faf63023c5 100644 --- a/app/src/organisms/EmergencyStop/EstopTakeover.tsx +++ b/app/src/organisms/EmergencyStop/EstopTakeover.tsx @@ -5,7 +5,7 @@ import { useEstopQuery } from '@opentrons/react-api-client' import { EstopPressedModal } from './EstopPressedModal' import { EstopMissingModal } from './EstopMissingModal' import { useEstopContext } from './hooks' -import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' +import { useIsUnboxingFlowOngoing } from '../ODD/hooks' import { getLocalRobot } from '../../redux/discovery' import { PHYSICALLY_ENGAGED, @@ -14,16 +14,33 @@ import { DISENGAGED, } from './constants' -const ESTOP_REFETCH_INTERVAL_MS = 10000 +const ESTOP_CURRENTLY_DISENGAGED_REFETCH_INTERVAL_MS = 10000 +const ESTOP_CURRENTLY_ENGAGED_REFETCH_INTERVAL_MS = 1000 interface EstopTakeoverProps { robotName?: string } export function EstopTakeover({ robotName }: EstopTakeoverProps): JSX.Element { + const [estopEngaged, setEstopEngaged] = React.useState(false) + const [ + isWaitingForLogicalDisengage, + setIsWaitingForLogicalDisengage, + ] = React.useState(false) const { data: estopStatus } = useEstopQuery({ - refetchInterval: ESTOP_REFETCH_INTERVAL_MS, + refetchInterval: estopEngaged + ? ESTOP_CURRENTLY_ENGAGED_REFETCH_INTERVAL_MS + : ESTOP_CURRENTLY_DISENGAGED_REFETCH_INTERVAL_MS, + onSuccess: response => { + setEstopEngaged( + [PHYSICALLY_ENGAGED || LOGICALLY_ENGAGED].includes( + response?.data.status + ) + ) + setIsWaitingForLogicalDisengage(false) + }, }) + const { isEmergencyStopModalDismissed, setIsEmergencyStopModalDismissed, @@ -47,6 +64,10 @@ export function EstopTakeover({ robotName }: EstopTakeoverProps): JSX.Element { closeModal={closeModal} isDismissedModal={isEmergencyStopModalDismissed} setIsDismissedModal={setIsEmergencyStopModalDismissed} + isWaitingForLogicalDisengage={isWaitingForLogicalDisengage} + setShouldSeeLogicalDisengage={() => { + setIsWaitingForLogicalDisengage(true) + }} /> ) case NOT_PRESENT: diff --git a/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx b/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx index 4a530858afe..2fd2733bea3 100644 --- a/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx +++ b/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx @@ -25,6 +25,8 @@ describe('EstopPressedModal - Touchscreen', () => { props = { isEngaged: true, closeModal: vi.fn(), + isWaitingForLogicalDisengage: false, + setShouldSeeLogicalDisengage: vi.fn(), } vi.mocked(getIsOnDevice).mockReturnValue(true) vi.mocked(useAcknowledgeEstopDisengageMutation).mockReturnValue({ @@ -69,6 +71,8 @@ describe('EstopPressedModal - Desktop', () => { closeModal: vi.fn(), isDismissedModal: false, setIsDismissedModal: vi.fn(), + isWaitingForLogicalDisengage: false, + setShouldSeeLogicalDisengage: vi.fn(), } vi.mocked(getIsOnDevice).mockReturnValue(false) vi.mocked(useAcknowledgeEstopDisengageMutation).mockReturnValue({ diff --git a/app/src/organisms/EmergencyStop/__tests__/EstopTakeover.test.tsx b/app/src/organisms/EmergencyStop/__tests__/EstopTakeover.test.tsx index 7e31c8fa54f..f169685ab65 100644 --- a/app/src/organisms/EmergencyStop/__tests__/EstopTakeover.test.tsx +++ b/app/src/organisms/EmergencyStop/__tests__/EstopTakeover.test.tsx @@ -8,7 +8,7 @@ import { useEstopQuery } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { EstopMissingModal } from '../EstopMissingModal' import { EstopPressedModal } from '../EstopPressedModal' -import { useIsUnboxingFlowOngoing } from '../../RobotSettingsDashboard/NetworkSettings/hooks' +import { useIsUnboxingFlowOngoing } from '../../ODD/hooks' import { ENGAGED, LOGICALLY_ENGAGED, @@ -22,7 +22,7 @@ import { EstopTakeover } from '../EstopTakeover' vi.mock('@opentrons/react-api-client') vi.mock('../EstopMissingModal') vi.mock('../EstopPressedModal') -vi.mock('../../RobotSettingsDashboard/NetworkSettings/hooks') +vi.mock('../../ODD/hooks') vi.mock('../../../redux/discovery') const mockPressed = { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index 5075d7e53f7..fa159677903 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -24,9 +24,11 @@ import { DropTipWizardFlows } from '../../DropTipWizardFlows' import { DT_ROUTES } from '../../DropTipWizardFlows/constants' import { SelectRecoveryOption } from './SelectRecoveryOption' -import type { PipetteWithTip } from '../../DropTipWizardFlows' import type { RecoveryContentProps, RecoveryRoute, RouteStep } from '../types' -import type { FixitCommandTypeUtils } from '../../DropTipWizardFlows/types' +import type { + FixitCommandTypeUtils, + PipetteWithTip, +} from '../../DropTipWizardFlows' // The Drop Tip flow entry point. Includes entry from SelectRecoveryOption and CancelRun. export function ManageTips(props: RecoveryContentProps): JSX.Element { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts index bee6eb0474c..ebe8a5f9bd9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts @@ -174,6 +174,7 @@ interface UseTipSelectionUtilsResult { tipSelectorDef: LabwareDefinition2 selectTips: (tipGroup: WellGroup) => void deselectTips: (locations: string[]) => void + areTipsSelected: boolean } // TODO(jh, 06-18-24): Enforce failure/warning when accessing tipSelectionUtils @@ -215,11 +216,15 @@ function useTipSelectionUtils( [] ) + const areTipsSelected = + selectedLocs != null && Object.keys(selectedLocs).length > 0 + return { selectedTipLocations: selectedLocs, tipSelectorDef, selectTips, deselectTips, + areTipsSelected, } } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts index eff2565a2eb..505ba6aff7c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts @@ -3,13 +3,12 @@ import head from 'lodash/head' import { useHost } from '@opentrons/react-api-client' import { getPipetteModelSpecs } from '@opentrons/shared-data' - import { useTipAttachmentStatus } from '../../DropTipWizardFlows' import type { Run, Instruments, PipetteData } from '@opentrons/api-client' import type { - TipAttachmentStatusResult, PipetteWithTip, + TipAttachmentStatusResult, } from '../../DropTipWizardFlows' interface UseRecoveryTipStatusProps { diff --git a/app/src/organisms/ErrorRecoveryFlows/index.tsx b/app/src/organisms/ErrorRecoveryFlows/index.tsx index bb5dd9af584..79b4ae90d07 100644 --- a/app/src/organisms/ErrorRecoveryFlows/index.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/index.tsx @@ -48,7 +48,7 @@ const INVALID_ER_RUN_STATUSES: RunStatus[] = [ RUN_STATUS_IDLE, ] -interface UseErrorRecoveryResult { +export interface UseErrorRecoveryResult { isERActive: boolean /* There is no FailedCommand if the run statis is not AWAITING_RECOVERY. */ failedCommand: FailedCommand | null diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx index d4012670c27..6bad4600c76 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx @@ -17,6 +17,7 @@ export function SelectTips(props: RecoveryContentProps): JSX.Element | null { routeUpdateActions, recoveryCommands, isOnDevice, + failedLabwareUtils, } = props const { ROBOT_PICKING_UP_TIPS } = RECOVERY_MAP const { pickUpTips } = recoveryCommands @@ -75,6 +76,7 @@ export function SelectTips(props: RecoveryContentProps): JSX.Element | null { + , getTopPortalEl() diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx index 15afe841639..bccc0567d8b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx @@ -49,6 +49,10 @@ describe('SelectTips', () => { channels: 8, }, } as any, + failedLabwareUtils: { + selectedTipLocations: { A1: null }, + areTipsSelected: true, + } as any, } vi.mocked(TipSelectionModal).mockReturnValue( @@ -138,4 +142,22 @@ describe('SelectTips', () => { }) expect(tertiaryBtn[0]).toBeDisabled() }) + + it('disables the primary button if tips are not selected', () => { + props = { + ...props, + failedLabwareUtils: { + selectedTipLocations: null, + areTipsSelected: false, + } as any, + } + + render(props) + + const primaryBtn = screen.getAllByRole('button', { + name: 'Pick up tips', + }) + + expect(primaryBtn[0]).toBeDisabled() + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx index 608c870324c..78f9666b3e0 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { describe, it, vi, beforeEach } from 'vitest' +import { describe, it, vi, beforeEach, expect } from 'vitest' import { screen } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' @@ -24,6 +24,10 @@ describe('TipSelectionModal', () => { ...mockRecoveryContentProps, allowTipSelection: true, toggleModal: vi.fn(), + failedLabwareUtils: { + selectedTipLocations: { A1: null }, + areTipsSelected: true, + } as any, } vi.mocked(TipSelection).mockReturnValue(
MOCK TIP SELECTION
) @@ -39,5 +43,17 @@ describe('TipSelectionModal', () => { render(props) screen.getByText('MOCK TIP SELECTION') + screen.getByLabelText('closeIcon') + }) + + it('prevents from users from exiting the modal if no well(s) are selected', () => { + props = { + ...props, + failedLabwareUtils: { areTipsSelected: false } as any, + } + + render(props) + + expect(screen.queryByLabelText('closeIcon')).not.toBeInTheDocument() }) }) diff --git a/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx b/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx index 33d581ea5e4..b47454a03d7 100644 --- a/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx +++ b/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx @@ -8,7 +8,7 @@ import { } from '@opentrons/react-api-client' import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { getTopPortalEl } from '../../App/portal' -import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' +import { useIsUnboxingFlowOngoing } from '../ODD/hooks' import { UpdateInProgressModal } from './UpdateInProgressModal' import { UpdateNeededModal } from './UpdateNeededModal' import type { Subsystem, InstrumentData } from '@opentrons/api-client' diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx index 3816b85261f..2cc4c53e5c9 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx @@ -12,7 +12,7 @@ import { import { i18n } from '../../../i18n' import { UpdateNeededModal } from '../UpdateNeededModal' import { UpdateInProgressModal } from '../UpdateInProgressModal' -import { useIsUnboxingFlowOngoing } from '../../RobotSettingsDashboard/NetworkSettings/hooks' +import { useIsUnboxingFlowOngoing } from '../../ODD/hooks' import { FirmwareUpdateTakeover } from '../FirmwareUpdateTakeover' import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' @@ -21,7 +21,7 @@ import type { BadPipette, PipetteData } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') vi.mock('../UpdateNeededModal') vi.mock('../UpdateInProgressModal') -vi.mock('../../RobotSettingsDashboard/NetworkSettings/hooks') +vi.mock('../../ODD/hooks') vi.mock('../../../resources/maintenance_runs') const render = () => { diff --git a/app/src/organisms/GripperWizardFlows/index.tsx b/app/src/organisms/GripperWizardFlows/index.tsx index 69068d9eb8c..84573117e39 100644 --- a/app/src/organisms/GripperWizardFlows/index.tsx +++ b/app/src/organisms/GripperWizardFlows/index.tsx @@ -15,16 +15,16 @@ import { useCreateMaintenanceCommandMutation, useDeleteMaintenanceRunMutation, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' +import { + useChainMaintenanceCommands, + useNotifyCurrentMaintenanceRun, +} from '../../resources/maintenance_runs' import { getTopPortalEl } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' import { FirmwareUpdateModal } from '../FirmwareUpdateModal' import { getIsOnDevice } from '../../redux/config' -import { - useChainMaintenanceCommands, - useCreateTargetedMaintenanceRunMutation, -} from '../../resources/runs' +import { useCreateTargetedMaintenanceRunMutation } from '../../resources/runs' import { getGripperWizardSteps } from './getGripperWizardSteps' import { GRIPPER_FLOW_TYPES, SECTIONS } from './constants' import { BeforeBeginning } from './BeforeBeginning' @@ -42,7 +42,9 @@ import type { InstrumentData, MaintenanceRun, CommandData, + RunStatus, } from '@opentrons/api-client' +import { RUN_STATUS_FAILED } from '@opentrons/api-client' import type { Coordinates, CreateCommand } from '@opentrons/shared-data' const RUN_REFETCH_INTERVAL = 5000 @@ -108,6 +110,7 @@ export function GripperWizardFlows( } }, [ maintenanceRunData?.data.id, + maintenanceRunData?.data.status, createdMaintenanceRunId, monitorMaintenanceRunForDeletion, closeFlow, @@ -160,6 +163,7 @@ export function GripperWizardFlows( flowType={flowType} createdMaintenanceRunId={createdMaintenanceRunId} maintenanceRunId={maintenanceRunData?.data.id} + maintenanceRunStatus={maintenanceRunData?.data.status} attachedGripper={attachedGripper} createMaintenanceRun={createTargetedMaintenanceRun} isCreateLoading={isCreateLoading} @@ -183,6 +187,7 @@ export function GripperWizardFlows( interface GripperWizardProps { flowType: GripperWizardFlowType maintenanceRunId?: string + maintenanceRunStatus?: RunStatus createdMaintenanceRunId: string | null attachedGripper: InstrumentData | null createMaintenanceRun: UseMutateFunction< @@ -212,6 +217,7 @@ export const GripperWizard = ( const { flowType, maintenanceRunId, + maintenanceRunStatus, createMaintenanceRun, handleCleanUpAndClose, handleClose, @@ -266,6 +272,7 @@ export const GripperWizard = ( } const sharedProps = { + maintenanceRunStatus, flowType, maintenanceRunId: maintenanceRunId != null && createdMaintenanceRunId === maintenanceRunId @@ -283,7 +290,7 @@ export const GripperWizard = ( let onExit if (currentStep == null) return null let modalContent: JSX.Element =
UNASSIGNED STEP
- if (showConfirmExit) { + if (showConfirmExit && maintenanceRunId !== null) { modalContent = ( ) - } else if (isExiting && errorMessage != null) { + } else if ( + (isExiting && errorMessage != null) || + maintenanceRunStatus === RUN_STATUS_FAILED + ) { onExit = handleClose modalContent = ( ) } else if (currentStep.section === SECTIONS.BEFORE_BEGINNING) { diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index b940cee7f3d..4f2a760582c 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -26,6 +26,7 @@ import { TC_MODULE_LOCATION_OT2, TC_MODULE_LOCATION_OT3, THERMOCYCLER_MODULE_TYPE, + inferModuleOrientationFromXCoordinate, getDeckDefFromRobotType, getLoadedLabwareDefinitionsByUri, getModuleType, @@ -213,7 +214,13 @@ export function MoveLabwareInterventionContent({ nestedLabwareDef, nestedLabwareId, }) => ( - + {nestedLabwareDef != null && nestedLabwareId !== command.params.labwareId ? ( diff --git a/app/src/organisms/LabwareCard/__tests__/LabwareCard.test.tsx b/app/src/organisms/LabwareCard/__tests__/LabwareCard.test.tsx index e0fdcc361ed..00cfd4f626b 100644 --- a/app/src/organisms/LabwareCard/__tests__/LabwareCard.test.tsx +++ b/app/src/organisms/LabwareCard/__tests__/LabwareCard.test.tsx @@ -6,14 +6,14 @@ import { nestedTextMatcher, } from '../../../__testing-utils__' import { i18n } from '../../../i18n' -import { useAllLabware } from '../../../pages/Labware/hooks' +import { useAllLabware } from '../../../pages/Desktop/Labware/hooks' import { mockDefinition } from '../../../redux/custom-labware/__fixtures__' import { CustomLabwareOverflowMenu } from '../CustomLabwareOverflowMenu' import { LabwareCard } from '..' import type * as OpentronsComponents from '@opentrons/components' -vi.mock('../../../pages/Labware/hooks') +vi.mock('../../../pages/Desktop/Labware/hooks') vi.mock('../CustomLabwareOverflowMenu') vi.mock('@opentrons/components', async importOriginal => { diff --git a/app/src/organisms/LabwareCard/index.tsx b/app/src/organisms/LabwareCard/index.tsx index c00a0362680..22b06710080 100644 --- a/app/src/organisms/LabwareCard/index.tsx +++ b/app/src/organisms/LabwareCard/index.tsx @@ -22,7 +22,7 @@ import { import { UNIVERSAL_FLAT_ADAPTER_X_DIMENSION } from '../LabwareDetails/Gallery' import { CustomLabwareOverflowMenu } from './CustomLabwareOverflowMenu' -import type { LabwareDefAndDate } from '../../pages/Labware/hooks' +import type { LabwareDefAndDate } from '../../pages/Desktop/Labware/hooks' export interface LabwareCardProps { labware: LabwareDefAndDate diff --git a/app/src/organisms/LabwareDetails/Dimensions.tsx b/app/src/organisms/LabwareDetails/Dimensions.tsx index c0ef65a3553..7a19466c9c7 100644 --- a/app/src/organisms/LabwareDetails/Dimensions.tsx +++ b/app/src/organisms/LabwareDetails/Dimensions.tsx @@ -4,7 +4,7 @@ import round from 'lodash/round' import { Box, SPACING, getFootprintDiagram } from '@opentrons/components' import { LabeledValue } from './StyledComponents/LabeledValue' import { ExpandingTitle } from './StyledComponents/ExpandingTitle' -import type { LabwareDefinition } from '../../pages/Labware/types' +import type { LabwareDefinition } from '../../pages/Desktop/Labware/types' const toFixed = (n: number): string => round(n, 2).toFixed(2) diff --git a/app/src/organisms/LabwareDetails/Gallery.tsx b/app/src/organisms/LabwareDetails/Gallery.tsx index 6cc4e10c85e..8bf8c8204f4 100644 --- a/app/src/organisms/LabwareDetails/Gallery.tsx +++ b/app/src/organisms/LabwareDetails/Gallery.tsx @@ -13,7 +13,7 @@ import { } from '@opentrons/components' import { labwareImages } from './labware-images' -import type { LabwareDefinition } from '../../pages/Labware/types' +import type { LabwareDefinition } from '../../pages/Desktop/Labware/types' export const UNIVERSAL_FLAT_ADAPTER_X_DIMENSION = 127.4 diff --git a/app/src/organisms/LabwareDetails/InsertDetails.tsx b/app/src/organisms/LabwareDetails/InsertDetails.tsx index 06120b3a3cf..c9a5adf3e10 100644 --- a/app/src/organisms/LabwareDetails/InsertDetails.tsx +++ b/app/src/organisms/LabwareDetails/InsertDetails.tsx @@ -12,7 +12,7 @@ import { WellProperties } from './WellProperties' import { WellDimensions } from './WellDimensions' import { ManufacturerDetails } from './ManufacturerDetails' -import type { LabwareDefinition } from '../../pages/Labware/types' +import type { LabwareDefinition } from '../../pages/Desktop/Labware/types' export interface InsertDetailsProps { definition: LabwareDefinition diff --git a/app/src/organisms/LabwareDetails/ManufacturerDetails.tsx b/app/src/organisms/LabwareDetails/ManufacturerDetails.tsx index 5bbd2035cac..d7ffbc1b6bc 100644 --- a/app/src/organisms/LabwareDetails/ManufacturerDetails.tsx +++ b/app/src/organisms/LabwareDetails/ManufacturerDetails.tsx @@ -12,7 +12,7 @@ import { SPACING, LegacyStyledText, } from '@opentrons/components' -import type { LabwareBrand } from '../../pages/Labware/types' +import type { LabwareBrand } from '../../pages/Desktop/Labware/types' export interface ManufacturerDetailsProps { brand: LabwareBrand diff --git a/app/src/organisms/LabwareDetails/WellDimensions.tsx b/app/src/organisms/LabwareDetails/WellDimensions.tsx index c056cb409f7..176cba33bf7 100644 --- a/app/src/organisms/LabwareDetails/WellDimensions.tsx +++ b/app/src/organisms/LabwareDetails/WellDimensions.tsx @@ -8,7 +8,7 @@ import { ExpandingTitle } from './StyledComponents/ExpandingTitle' import type { LabwareWellGroupProperties, LabwareParameters, -} from '../../pages/Labware/types' +} from '../../pages/Desktop/Labware/types' const toFixed = (n: number): string => round(n, 2).toFixed(2) diff --git a/app/src/organisms/LabwareDetails/WellProperties.tsx b/app/src/organisms/LabwareDetails/WellProperties.tsx index aa20ad8f0dd..112e4f538e7 100644 --- a/app/src/organisms/LabwareDetails/WellProperties.tsx +++ b/app/src/organisms/LabwareDetails/WellProperties.tsx @@ -17,7 +17,7 @@ import type { LabwareDefinition, LabwareWellGroupProperties, LabwareVolumeUnits, -} from '../../pages/Labware/types' +} from '../../pages/Desktop/Labware/types' export interface AllWellPropertiesProps { definition: LabwareDefinition diff --git a/app/src/organisms/LabwareDetails/WellSpacing.tsx b/app/src/organisms/LabwareDetails/WellSpacing.tsx index 2eb9bb6b028..86089ae16d8 100644 --- a/app/src/organisms/LabwareDetails/WellSpacing.tsx +++ b/app/src/organisms/LabwareDetails/WellSpacing.tsx @@ -5,7 +5,7 @@ import { getSpacingDiagram } from '@opentrons/components' import { LabeledValue } from './StyledComponents/LabeledValue' import { ExpandingTitle } from './StyledComponents/ExpandingTitle' -import type { LabwareWellGroupProperties } from '../../pages/Labware/types' +import type { LabwareWellGroupProperties } from '../../pages/Desktop/Labware/types' const toFixed = (n: number): string => round(n, 2).toFixed(2) diff --git a/app/src/organisms/LabwareDetails/__tests__/LabwareDetails.test.tsx b/app/src/organisms/LabwareDetails/__tests__/LabwareDetails.test.tsx index 6567b404287..d3bf97bf5ef 100644 --- a/app/src/organisms/LabwareDetails/__tests__/LabwareDetails.test.tsx +++ b/app/src/organisms/LabwareDetails/__tests__/LabwareDetails.test.tsx @@ -4,7 +4,7 @@ import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' -import { useAllLabware } from '../../../pages/Labware/hooks' +import { useAllLabware } from '../../../pages/Desktop/Labware/hooks' import { mockOpentronsLabwareDetailsDefinition } from '../../../redux/custom-labware/__fixtures__' import { CustomLabwareOverflowMenu } from '../../LabwareCard/CustomLabwareOverflowMenu' import { Dimensions } from '../Dimensions' @@ -17,7 +17,7 @@ import { WellSpacing } from '../WellSpacing' import { LabwareDetails } from '..' -vi.mock('../../../pages/Labware/hooks') +vi.mock('../../../pages/Desktop/Labware/hooks') vi.mock('../../LabwareCard/CustomLabwareOverflowMenu') vi.mock('../Dimensions') vi.mock('../Gallery') diff --git a/app/src/organisms/LabwareDetails/helpers/labels.ts b/app/src/organisms/LabwareDetails/helpers/labels.ts index 06590d2ad80..1397725665b 100644 --- a/app/src/organisms/LabwareDetails/helpers/labels.ts +++ b/app/src/organisms/LabwareDetails/helpers/labels.ts @@ -2,7 +2,7 @@ import uniqBy from 'lodash/uniqBy' import type { LabwareWellGroupProperties, LabwareDefinition, -} from '../../../pages/Labware/types' +} from '../../../pages/Desktop/Labware/types' const WELL_TYPE_BY_CATEGORY = { tubeRack: 'tube', tipRack: 'tip', diff --git a/app/src/organisms/LabwareDetails/index.tsx b/app/src/organisms/LabwareDetails/index.tsx index dbc2ff0bd39..8da23ed47b0 100644 --- a/app/src/organisms/LabwareDetails/index.tsx +++ b/app/src/organisms/LabwareDetails/index.tsx @@ -35,7 +35,7 @@ import { ManufacturerDetails } from './ManufacturerDetails' import { InsertDetails } from './InsertDetails' import { Gallery } from './Gallery' import { CustomLabwareOverflowMenu } from '../LabwareCard/CustomLabwareOverflowMenu' -import type { LabwareDefAndDate } from '../../pages/Labware/hooks' +import type { LabwareDefAndDate } from '../../pages/Desktop/Labware/hooks' const CLOSE_ICON_STYLE = css` border-radius: 50%; diff --git a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx index 54b0503c19d..50d6d214bc9 100644 --- a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx +++ b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx @@ -25,69 +25,91 @@ import { WizardHeader } from '../../molecules/WizardHeader' import { i18n } from '../../i18n' const SUPPORT_EMAIL = 'support@opentrons.com' - -interface FatalErrorModalProps { +interface FatalErrorProps { errorMessage: string shouldUseMetalProbe: boolean onClose: () => void } + +interface FatalErrorModalProps extends FatalErrorProps { + isOnDevice: boolean +} + export function FatalErrorModal(props: FatalErrorModalProps): JSX.Element { - const { errorMessage, shouldUseMetalProbe, onClose } = props const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) + const { onClose, isOnDevice } = props return createPortal( - - } - > - + + ) : ( + + } > - - - {i18n.format(t('shared:something_went_wrong'), 'sentenceCase')} - - {shouldUseMetalProbe ? ( - - {t('remove_probe_before_exit')} - - ) : null} - - {t('branded:help_us_improve_send_error_report', { - support_email: SUPPORT_EMAIL, - })} - - - - {t('shared:exit')} - -
- , + + + ), getTopPortalEl() ) } +export function FatalError(props: FatalErrorProps): JSX.Element { + const { errorMessage, shouldUseMetalProbe, onClose } = props + const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) + return ( + + + + {i18n.format(t('shared:something_went_wrong'), 'sentenceCase')} + + {shouldUseMetalProbe ? ( + + {t('remove_probe_before_exit')} + + ) : null} + + {t('branded:help_us_improve_send_error_report', { + support_email: SUPPORT_EMAIL, + })} + + + + {t('shared:exit')} + + + ) +} + const ErrorHeader = styled.h1` text-align: ${TEXT_ALIGN_CENTER}; ${TYPOGRAPHY.h1Default} diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx index 9b6e232af68..b63c87ecdf9 100644 --- a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -23,10 +23,12 @@ import { DetachProbe } from './DetachProbe' import { PickUpTip } from './PickUpTip' import { ReturnTip } from './ReturnTip' import { ResultsSummary } from './ResultsSummary' -import { useChainMaintenanceCommands } from '../../resources/runs' -import { FatalErrorModal } from './FatalErrorModal' +import { FatalError } from './FatalErrorModal' import { RobotMotionLoader } from './RobotMotionLoader' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' +import { + useChainMaintenanceCommands, + useNotifyCurrentMaintenanceRun, +} from '../../resources/maintenance_runs' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import type { @@ -333,10 +335,10 @@ export const LabwarePositionCheckComponent = ( ) } else if (fatalError != null) { modalContent = ( - ) } else if (showConfirmation) { @@ -413,18 +415,12 @@ export const LabwarePositionCheckComponent = ( const wizardHeader = ( { - if (fatalError != null) { - handleCleanUpAndClose() - } else { - confirmExitLPC() - } - } + : confirmExitLPC } /> ) diff --git a/app/src/organisms/LabwarePositionCheck/index.tsx b/app/src/organisms/LabwarePositionCheck/index.tsx index abadfa346a1..5648913cfe2 100644 --- a/app/src/organisms/LabwarePositionCheck/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/index.tsx @@ -2,6 +2,8 @@ import * as React from 'react' import { useLogger } from '../../logger' import { LabwarePositionCheckComponent } from './LabwarePositionCheckComponent' import { FatalErrorModal } from './FatalErrorModal' +import { getIsOnDevice } from '../../redux/config' +import { useSelector } from 'react-redux' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import type { @@ -31,12 +33,14 @@ export const LabwarePositionCheck = ( props: LabwarePositionCheckModalProps ): JSX.Element => { const logger = useLogger(new URL('', import.meta.url).pathname) + const isOnDevice = useSelector(getIsOnDevice) return ( @@ -52,7 +56,9 @@ interface ErrorBoundaryProps { errorMessage: string shouldUseMetalProbe: boolean onClose: () => void + isOnDevice: boolean }) => JSX.Element + isOnDevice: boolean } class ErrorBoundary extends React.Component< ErrorBoundaryProps, @@ -74,7 +80,12 @@ class ErrorBoundary extends React.Component< } render(): ErrorBoundaryProps['children'] | JSX.Element { - const { ErrorComponent, children, shouldUseMetalProbe } = this.props + const { + ErrorComponent, + children, + shouldUseMetalProbe, + isOnDevice, + } = this.props const { error } = this.state if (error != null) return ( @@ -82,6 +93,7 @@ class ErrorBoundary extends React.Component< errorMessage={error.message} shouldUseMetalProbe={shouldUseMetalProbe} onClose={this.props.onClose} + isOnDevice={isOnDevice} /> ) // Normally, just render children diff --git a/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx b/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx index 8797f0aebc0..ecd63653dfe 100644 --- a/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx +++ b/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx @@ -28,7 +28,7 @@ export function setHeaterShakerAttached( heaterShakerAttached ) } -interface ConfirmAttachmentModalProps { +export interface ConfirmAttachmentModalProps { onCloseClick: () => void isProceedToRunModal: boolean onConfirmClick: () => void diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx index 4dc583d6c9d..0e48c4828cc 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx @@ -237,9 +237,7 @@ describe('ModuleCard', () => { eatToast: mockEatToast, }) vi.mocked(getRequestById).mockReturnValue(null) - when(useCurrentRunStatus) - .calledWith(expect.any(Object)) - .thenReturn(RUN_STATUS_IDLE) + when(useCurrentRunStatus).calledWith().thenReturn(RUN_STATUS_IDLE) when(useIsFlex).calledWith(props.robotName).thenReturn(true) when(useIsEstopNotDisengaged).calledWith(props.robotName).thenReturn(false) }) @@ -311,9 +309,7 @@ describe('ModuleCard', () => { }) it('renders kebab icon and it is disabled when run is in progress', () => { - when(useCurrentRunStatus) - .calledWith(expect.any(Object)) - .thenReturn(RUN_STATUS_RUNNING) + when(useCurrentRunStatus).calledWith().thenReturn(RUN_STATUS_RUNNING) render({ ...props, module: mockMagneticModule, diff --git a/app/src/organisms/ModuleCard/index.tsx b/app/src/organisms/ModuleCard/index.tsx index 52b4b000374..9d02e55d562 100644 --- a/app/src/organisms/ModuleCard/index.tsx +++ b/app/src/organisms/ModuleCard/index.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { useNavigate } from 'react-router-dom' import { ALIGN_START, @@ -126,14 +125,8 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { const [showCalModal, setShowCalModal] = React.useState(false) const [targetProps, tooltipProps] = useHoverTooltip() - const navigate = useNavigate() - const runStatus = useCurrentRunStatus({ - onSettled: data => { - if (data == null) { - navigate('/upload') - } - }, - }) + + const runStatus = useCurrentRunStatus() const isFlex = useIsFlex(robotName) const requireModuleCalibration = isFlex && @@ -272,6 +265,7 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { closeFlow={() => { setShowCalModal(false) }} + isLoadedInRun={isLoadedInRun} isPrepCommandLoading={isCommandMutationLoading} prepCommandErrorMessage={ prepCommandErrorMessage === '' ? undefined : prepCommandErrorMessage diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index 0b110f3bce4..dc56e3891f5 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -47,6 +47,7 @@ interface SelectLocationProps extends ModuleCalibrationWizardStepProps { occupiedCutouts: CutoutConfig[] deckConfig: DeckConfiguration configuredFixtureIdByCutoutId: { [cutoutId in CutoutId]?: CutoutFixtureId } + isLoadedInRun: boolean } export const SelectLocation = ( props: SelectLocationProps @@ -56,6 +57,7 @@ export const SelectLocation = ( attachedModule, deckConfig, configuredFixtureIdByCutoutId, + isLoadedInRun, } = props const { t } = useTranslation('module_wizard_flows') const moduleName = getModuleDisplayName(attachedModule.moduleModel) @@ -93,6 +95,8 @@ export const SelectLocation = ( cutoutFixtureId ) && attachedModule.serialNumber === opentronsModuleSerialNumber if ( + // in run setup, module calibration only available when module location is already correctly configured + !isLoadedInRun && mayMountToCutoutIds.includes(cutoutId) && (isCurrentConfiguration || SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId)) diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 1b1fe62221a..6e18d0ab851 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -17,10 +17,7 @@ import { import { getTopPortalEl } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' import { useAttachedPipettesFromInstrumentsQuery } from '../../organisms/Devices/hooks' -import { - useChainMaintenanceCommands, - useCreateTargetedMaintenanceRunMutation, -} from '../../resources/runs' +import { useCreateTargetedMaintenanceRunMutation } from '../../resources/runs' import { getIsOnDevice } from '../../redux/config' import { SimpleWizardBody, @@ -35,9 +32,13 @@ import { SelectLocation } from './SelectLocation' import { Success } from './Success' import { DetachProbe } from './DetachProbe' import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configuration' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' +import { + useChainMaintenanceCommands, + useNotifyCurrentMaintenanceRun, +} from '../../resources/maintenance_runs' import type { AttachedModule, CommandData } from '@opentrons/api-client' +import { RUN_STATUS_FAILED } from '@opentrons/api-client' import type { CreateCommand, CutoutConfig, @@ -48,6 +49,7 @@ interface ModuleWizardFlowsProps { attachedModule: AttachedModule closeFlow: () => void isPrepCommandLoading: boolean + isLoadedInRun?: boolean onComplete?: () => void prepCommandErrorMessage?: string } @@ -59,6 +61,7 @@ export const ModuleWizardFlows = ( ): JSX.Element | null => { const { attachedModule, + isLoadedInRun = false, isPrepCommandLoading, closeFlow, onComplete, @@ -270,7 +273,11 @@ export const ModuleWizardFlows = ( })} /> ) - } else if (prepCommandErrorMessage != null || errorMessage != null) { + } else if ( + prepCommandErrorMessage != null || + errorMessage != null || + maintenanceRunData?.data.status === RUN_STATUS_FAILED + ) { modalContent = ( diff --git a/app/src/organisms/Navigation/index.tsx b/app/src/organisms/Navigation/index.tsx index a9a55f53e63..e03a5f443d0 100644 --- a/app/src/organisms/Navigation/index.tsx +++ b/app/src/organisms/Navigation/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { NavLink } from 'react-router-dom' +import { useLocation, NavLink } from 'react-router-dom' import styled from 'styled-components' import { @@ -39,15 +39,19 @@ const NAV_LINKS: Array = [ '/robot-settings', ] -// TODO(sb:7/10/24): update this wrapper to fade on both sides only when not scrolled completely to that side -// This will be accomplished in PLAT-399 const CarouselWrapper = styled.div` display: ${DISPLAY_FLEX}; flex-direction: ${DIRECTION_ROW}; align-items: ${ALIGN_FLEX_START}; - width: 42.25rem; + width: 56.75rem; overflow-x: ${OVERFLOW_SCROLL}; - -webkit-mask-image: linear-gradient(90deg, #000 90%, transparent); + -webkit-mask-image: linear-gradient( + to right, + transparent 0%, + black 0%, + black 96.5%, + transparent 100% + ); &::-webkit-scrollbar { display: none; } @@ -65,6 +69,7 @@ interface NavigationProps { export function Navigation(props: NavigationProps): JSX.Element { const { setNavMenuIsOpened, longPressModalIsOpened } = props const { t } = useTranslation('top_navigation') + const location = useLocation() const localRobot = useSelector(getLocalRobot) const [showNavMenu, setShowNavMenu] = React.useState(false) const robotName = localRobot?.name != null ? localRobot.name : 'no name' @@ -95,6 +100,15 @@ export function Navigation(props: NavigationProps): JSX.Element { if (scrollRef.current != null) { observer.observe(scrollRef.current) } + + const navBarScrollRef = React.useRef(null) + React.useEffect(() => { + navBarScrollRef?.current?.scrollIntoView({ + behavior: 'auto', + inline: 'center', + }) + }, []) + function getPathDisplayName(path: typeof NAV_LINKS[number]): string { switch (path) { case '/instruments': @@ -134,35 +148,40 @@ export function Navigation(props: NavigationProps): JSX.Element { aria-label="Navigation_container" > - - - {iconName != null ? ( - - ) : null} - - {NAV_LINKS.map(path => ( + + + {iconName != null ? ( + + ) : null} + {NAV_LINKS.map(path => ( + + + ))} diff --git a/app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx b/app/src/organisms/ODD/NameRobot/ConfirmRobotName.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx rename to app/src/organisms/ODD/NameRobot/ConfirmRobotName.tsx diff --git a/app/src/organisms/OnDeviceDisplay/NameRobot/__tests__/ConfirmRobotName.test.tsx b/app/src/organisms/ODD/NameRobot/__tests__/ConfirmRobotName.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/NameRobot/__tests__/ConfirmRobotName.test.tsx rename to app/src/organisms/ODD/NameRobot/__tests__/ConfirmRobotName.test.tsx diff --git a/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx b/app/src/organisms/ODD/NetworkSettings/AlternativeSecurityTypeModal.tsx similarity index 90% rename from app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx rename to app/src/organisms/ODD/NetworkSettings/AlternativeSecurityTypeModal.tsx index 8709d9bdca5..3d85d2dddc4 100644 --- a/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx +++ b/app/src/organisms/ODD/NetworkSettings/AlternativeSecurityTypeModal.tsx @@ -12,10 +12,10 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { SmallButton } from '../../atoms/buttons' -import { OddModal } from '../../molecules/OddModal' +import { SmallButton } from '../../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' interface AlternativeSecurityTypeModalProps { setShowAlternativeSecurityTypeModal: ( diff --git a/app/src/organisms/NetworkSettings/ConnectingNetwork.tsx b/app/src/organisms/ODD/NetworkSettings/ConnectingNetwork.tsx similarity index 100% rename from app/src/organisms/NetworkSettings/ConnectingNetwork.tsx rename to app/src/organisms/ODD/NetworkSettings/ConnectingNetwork.tsx diff --git a/app/src/organisms/NetworkSettings/DisplaySearchNetwork.tsx b/app/src/organisms/ODD/NetworkSettings/DisplaySearchNetwork.tsx similarity index 100% rename from app/src/organisms/NetworkSettings/DisplaySearchNetwork.tsx rename to app/src/organisms/ODD/NetworkSettings/DisplaySearchNetwork.tsx diff --git a/app/src/organisms/NetworkSettings/DisplayWifiList.tsx b/app/src/organisms/ODD/NetworkSettings/DisplayWifiList.tsx similarity index 93% rename from app/src/organisms/NetworkSettings/DisplayWifiList.tsx rename to app/src/organisms/ODD/NetworkSettings/DisplayWifiList.tsx index 2925a47f392..476db7169d5 100644 --- a/app/src/organisms/NetworkSettings/DisplayWifiList.tsx +++ b/app/src/organisms/ODD/NetworkSettings/DisplayWifiList.tsx @@ -17,11 +17,11 @@ import { LegacyStyledText, } from '@opentrons/components' -import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants' -import { RobotSetupHeader } from '../../organisms/RobotSetupHeader' +import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons/constants' +import { RobotSetupHeader } from '../../../organisms/RobotSetupHeader' import { DisplaySearchNetwork } from './DisplaySearchNetwork' -import type { WifiNetwork } from '../../redux/networking/types' +import type { WifiNetwork } from '../../../redux/networking/types' const NETWORK_ROW_STYLE = css` display: ${DISPLAY_FLEX}; diff --git a/app/src/organisms/NetworkSettings/FailedToConnect.tsx b/app/src/organisms/ODD/NetworkSettings/FailedToConnect.tsx similarity index 95% rename from app/src/organisms/NetworkSettings/FailedToConnect.tsx rename to app/src/organisms/ODD/NetworkSettings/FailedToConnect.tsx index aa334813352..2cfdd9092d8 100644 --- a/app/src/organisms/NetworkSettings/FailedToConnect.tsx +++ b/app/src/organisms/ODD/NetworkSettings/FailedToConnect.tsx @@ -14,9 +14,9 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { MediumButton } from '../../atoms/buttons' +import { MediumButton } from '../../../atoms/buttons' -import type { RequestState } from '../../redux/robot-api/types' +import type { RequestState } from '../../../redux/robot-api/types' interface FailedToConnectProps { selectedSsid: string diff --git a/app/src/organisms/NetworkSettings/SelectAuthenticationType.tsx b/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx similarity index 93% rename from app/src/organisms/NetworkSettings/SelectAuthenticationType.tsx rename to app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx index 39906414758..455c91d432f 100644 --- a/app/src/organisms/NetworkSettings/SelectAuthenticationType.tsx +++ b/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx @@ -16,13 +16,13 @@ import { RadioButton, } from '@opentrons/components' -import { getLocalRobot } from '../../redux/discovery' -import { getNetworkInterfaces, fetchStatus } from '../../redux/networking' -import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' +import { getLocalRobot } from '../../../redux/discovery' +import { getNetworkInterfaces, fetchStatus } from '../../../redux/networking' +import { useIsUnboxingFlowOngoing } from '../hooks' import { AlternativeSecurityTypeModal } from './AlternativeSecurityTypeModal' import type { WifiSecurityType } from '@opentrons/api-client' -import type { Dispatch, State } from '../../redux/types' +import type { Dispatch, State } from '../../../redux/types' interface SelectAuthenticationTypeProps { selectedAuthType: WifiSecurityType diff --git a/app/src/organisms/NetworkSettings/SetWifiCred.tsx b/app/src/organisms/ODD/NetworkSettings/SetWifiCred.tsx similarity index 95% rename from app/src/organisms/NetworkSettings/SetWifiCred.tsx rename to app/src/organisms/ODD/NetworkSettings/SetWifiCred.tsx index aa0bc30f717..7d1af2ab2e8 100644 --- a/app/src/organisms/NetworkSettings/SetWifiCred.tsx +++ b/app/src/organisms/ODD/NetworkSettings/SetWifiCred.tsx @@ -18,8 +18,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { FullKeyboard } from '../../atoms/SoftwareKeyboard' -import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' +import { FullKeyboard } from '../../../atoms/SoftwareKeyboard' +import { useIsUnboxingFlowOngoing } from '../hooks' interface SetWifiCredProps { password: string diff --git a/app/src/organisms/NetworkSettings/SetWifiSsid.tsx b/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx similarity index 91% rename from app/src/organisms/NetworkSettings/SetWifiSsid.tsx rename to app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx index 7f987144cb4..05c0b7a13a2 100644 --- a/app/src/organisms/NetworkSettings/SetWifiSsid.tsx +++ b/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx @@ -12,8 +12,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { FullKeyboard } from '../../atoms/SoftwareKeyboard' -import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' +import { FullKeyboard } from '../../../atoms/SoftwareKeyboard' +import { useIsUnboxingFlowOngoing } from '../hooks' interface SetWifiSsidProps { errorMessage?: string | null diff --git a/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx b/app/src/organisms/ODD/NetworkSettings/WifiConnectionDetails.tsx similarity index 92% rename from app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx rename to app/src/organisms/ODD/NetworkSettings/WifiConnectionDetails.tsx index ec95615c4e5..e3cb9e18ba6 100644 --- a/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx +++ b/app/src/organisms/ODD/NetworkSettings/WifiConnectionDetails.tsx @@ -17,14 +17,14 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { MediumButton } from '../../atoms/buttons' -import { RobotSetupHeader } from '../../organisms/RobotSetupHeader' -import { getLocalRobot } from '../../redux/discovery' -import { getNetworkInterfaces, fetchStatus } from '../../redux/networking' +import { MediumButton } from '../../../atoms/buttons' +import { RobotSetupHeader } from '../../../organisms/RobotSetupHeader' +import { getLocalRobot } from '../../../redux/discovery' +import { getNetworkInterfaces, fetchStatus } from '../../../redux/networking' import { NetworkDetailsModal } from '../RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal' import type { WifiSecurityType } from '@opentrons/api-client' -import type { Dispatch, State } from '../../redux/types' +import type { Dispatch, State } from '../../../redux/types' interface WifiConnectionDetailsProps { ssid?: string diff --git a/app/src/organisms/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx similarity index 94% rename from app/src/organisms/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx index ee23afbee84..4c091f8e0ae 100644 --- a/app/src/organisms/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { AlternativeSecurityTypeModal } from '../AlternativeSecurityTypeModal' import type { NavigateFunction } from 'react-router-dom' diff --git a/app/src/organisms/NetworkSettings/__tests__/ConnectingNetwork.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/ConnectingNetwork.test.tsx similarity index 89% rename from app/src/organisms/NetworkSettings/__tests__/ConnectingNetwork.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/ConnectingNetwork.test.tsx index 9d19cba1822..32bcd2e81cb 100644 --- a/app/src/organisms/NetworkSettings/__tests__/ConnectingNetwork.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/ConnectingNetwork.test.tsx @@ -3,8 +3,8 @@ import { MemoryRouter } from 'react-router-dom' import { screen } from '@testing-library/react' import { beforeEach, describe, expect, it } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { ConnectingNetwork } from '../ConnectingNetwork' const render = (props: React.ComponentProps) => { diff --git a/app/src/organisms/NetworkSettings/__tests__/DisplaySearchNetwork.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/DisplaySearchNetwork.test.tsx similarity index 85% rename from app/src/organisms/NetworkSettings/__tests__/DisplaySearchNetwork.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/DisplaySearchNetwork.test.tsx index e582356a474..e82aedb63ca 100644 --- a/app/src/organisms/NetworkSettings/__tests__/DisplaySearchNetwork.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/DisplaySearchNetwork.test.tsx @@ -4,8 +4,8 @@ import { describe, expect, it } from 'vitest' import { COLORS } from '@opentrons/components' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { DisplaySearchNetwork } from '../DisplaySearchNetwork' const render = () => { diff --git a/app/src/organisms/NetworkSettings/__tests__/DisplayWifiList.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/DisplayWifiList.test.tsx similarity index 89% rename from app/src/organisms/NetworkSettings/__tests__/DisplayWifiList.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/DisplayWifiList.test.tsx index 2a901ee1850..1970dc85242 100644 --- a/app/src/organisms/NetworkSettings/__tests__/DisplayWifiList.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/DisplayWifiList.test.tsx @@ -2,9 +2,9 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import * as Fixtures from '../../../redux/networking/__fixtures__' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import * as Fixtures from '../../../../redux/networking/__fixtures__' import { DisplaySearchNetwork } from '../DisplaySearchNetwork' import { DisplayWifiList } from '../DisplayWifiList' @@ -20,8 +20,8 @@ const mockWifiList = [ }, ] -vi.mock('../../../redux/networking/selectors') -vi.mock('../../../redux/discovery/selectors') +vi.mock('../../../../redux/networking/selectors') +vi.mock('../../../../redux/discovery/selectors') vi.mock('../DisplaySearchNetwork') vi.mock('react-router-dom', async importOriginal => { const actual = await importOriginal() diff --git a/app/src/organisms/NetworkSettings/__tests__/FailedToConnect.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/FailedToConnect.test.tsx similarity index 91% rename from app/src/organisms/NetworkSettings/__tests__/FailedToConnect.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/FailedToConnect.test.tsx index 22a9a4d9440..894eac79887 100644 --- a/app/src/organisms/NetworkSettings/__tests__/FailedToConnect.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/FailedToConnect.test.tsx @@ -3,11 +3,11 @@ import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { FailedToConnect } from '../FailedToConnect' -import type { RequestState } from '../../../redux/robot-api/types' +import type { RequestState } from '../../../../redux/robot-api/types' const render = (props: React.ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx similarity index 85% rename from app/src/organisms/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx index d014f2b5316..00b53dfd25f 100644 --- a/app/src/organisms/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx @@ -3,10 +3,13 @@ import { fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { afterEach, beforeEach, describe, it, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { getNetworkInterfaces, INTERFACE_WIFI } from '../../../redux/networking' -import { useIsUnboxingFlowOngoing } from '../../RobotSettingsDashboard/NetworkSettings/hooks' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { + getNetworkInterfaces, + INTERFACE_WIFI, +} from '../../../../redux/networking' +import { useIsUnboxingFlowOngoing } from '../../hooks' import { AlternativeSecurityTypeModal } from '../AlternativeSecurityTypeModal' import { SelectAuthenticationType } from '../SelectAuthenticationType' import { SetWifiCred } from '../SetWifiCred' @@ -17,10 +20,10 @@ const mockNavigate = vi.fn() const mockSetSelectedAuthType = vi.fn() vi.mock('../SetWifiCred') -vi.mock('../../../redux/networking') -vi.mock('../../../redux/discovery/selectors') +vi.mock('../../../../redux/networking') +vi.mock('../../../../redux/discovery/selectors') vi.mock('../AlternativeSecurityTypeModal') -vi.mock('../../RobotSettingsDashboard/NetworkSettings/hooks') +vi.mock('../../hooks') vi.mock('react-router-dom', async importOriginal => { const actual = await importOriginal() return { diff --git a/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiCred.test.tsx similarity index 90% rename from app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiCred.test.tsx index 51444bea730..9c2d6867ab8 100644 --- a/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiCred.test.tsx @@ -3,13 +3,13 @@ import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { SetWifiCred } from '../SetWifiCred' const mockSetPassword = vi.fn() -vi.mock('../../../redux/discovery') -vi.mock('../../../redux/robot-api') +vi.mock('../../../../redux/discovery') +vi.mock('../../../../redux/robot-api') const render = (props: React.ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/NetworkSettings/__tests__/SetWifiSsid.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiSsid.test.tsx similarity index 93% rename from app/src/organisms/NetworkSettings/__tests__/SetWifiSsid.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiSsid.test.tsx index 761364da978..017155a8fe2 100644 --- a/app/src/organisms/NetworkSettings/__tests__/SetWifiSsid.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiSsid.test.tsx @@ -3,8 +3,8 @@ import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { SetWifiSsid } from '../SetWifiSsid' const mockSetSelectedSsid = vi.fn() diff --git a/app/src/organisms/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx similarity index 84% rename from app/src/organisms/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx rename to app/src/organisms/ODD/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx index 3c5427d3426..e1426610ebf 100644 --- a/app/src/organisms/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx @@ -3,19 +3,22 @@ import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useWifiList } from '../../../resources/networking/hooks' -import { getNetworkInterfaces, INTERFACE_WIFI } from '../../../redux/networking' -import * as Fixtures from '../../../redux/networking/__fixtures__' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { useWifiList } from '../../../../resources/networking/hooks' +import { + getNetworkInterfaces, + INTERFACE_WIFI, +} from '../../../../redux/networking' +import * as Fixtures from '../../../../redux/networking/__fixtures__' import { NetworkDetailsModal } from '../../RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal' import { WifiConnectionDetails } from '../WifiConnectionDetails' import type { NavigateFunction } from 'react-router-dom' -vi.mock('../../../resources/networking/hooks') -vi.mock('../../../redux/networking') -vi.mock('../../../redux/discovery/selectors') +vi.mock('../../../../resources/networking/hooks') +vi.mock('../../../../redux/networking') +vi.mock('../../../../redux/discovery/selectors') vi.mock('../../RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal') const mockNavigate = vi.fn() diff --git a/app/src/organisms/NetworkSettings/index.ts b/app/src/organisms/ODD/NetworkSettings/index.ts similarity index 100% rename from app/src/organisms/NetworkSettings/index.ts rename to app/src/organisms/ODD/NetworkSettings/index.ts diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolDetails/ProtocolDetailsSkeleton.tsx b/app/src/organisms/ODD/ProtocolDetails/ProtocolDetailsSkeleton.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/ProtocolDetails/ProtocolDetailsSkeleton.tsx rename to app/src/organisms/ODD/ProtocolDetails/ProtocolDetailsSkeleton.tsx diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetailsSkeleton.test.tsx b/app/src/organisms/ODD/ProtocolDetails/__tests__/ProtocolDetailsSkeleton.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetailsSkeleton.test.tsx rename to app/src/organisms/ODD/ProtocolDetails/__tests__/ProtocolDetailsSkeleton.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolDetails/index.ts b/app/src/organisms/ODD/ProtocolDetails/index.ts similarity index 100% rename from app/src/organisms/OnDeviceDisplay/ProtocolDetails/index.ts rename to app/src/organisms/ODD/ProtocolDetails/index.ts diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx similarity index 85% rename from app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx index ef226bbfc19..499ea4cf717 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx @@ -9,11 +9,11 @@ import { useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { useMostRecentCompletedAnalysis } from '../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ProtocolSetupDeckConfiguration } from '..' -import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' +import { useNotifyDeckConfigurationQuery } from '../../../../../resources/deck_configuration' import type { UseQueryResult } from 'react-query' import type { @@ -24,8 +24,8 @@ import type { Modules } from '@opentrons/api-client' vi.mock('@opentrons/components/src/hardware-sim/BaseDeck/index') vi.mock('@opentrons/react-api-client') -vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') -vi.mock('../../../resources/deck_configuration') +vi.mock('../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') +vi.mock('../../../../../resources/deck_configuration') const mockSetSetupScreen = vi.fn() const PROTOCOL_DETAILS = { diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx similarity index 88% rename from app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx index 58bda580d75..c18604f51a8 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx @@ -19,12 +19,12 @@ import { getSimplestDeckConfigForProtocol, } from '@opentrons/shared-data' -import { ChildNavigation } from '../ChildNavigation' -import { AddFixtureModal } from '../DeviceDetailsDeckConfiguration/AddFixtureModal' -import { DeckConfigurationDiscardChangesModal } from '../DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' -import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { getTopPortalEl } from '../../App/portal' -import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configuration' +import { ChildNavigation } from '../../../ChildNavigation' +import { AddFixtureModal } from '../../../DeviceDetailsDeckConfiguration/AddFixtureModal' +import { DeckConfigurationDiscardChangesModal } from '../../../DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' +import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { getTopPortalEl } from '../../../../App/portal' +import { useNotifyDeckConfigurationQuery } from '../../../../resources/deck_configuration' import type { CutoutFixtureId, @@ -32,7 +32,7 @@ import type { ModuleModel, } from '@opentrons/shared-data' import type { ModuleOnDeck } from '@opentrons/components' -import type { SetupScreens } from '../../pages/ProtocolSetup' +import type { SetupScreens } from '../types' interface ProtocolSetupDeckConfigurationProps { cutoutId: CutoutId | null diff --git a/app/src/organisms/ProtocolSetupInstruments/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/ProtocolSetupInstruments.tsx similarity index 84% rename from app/src/organisms/ProtocolSetupInstruments/index.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/ProtocolSetupInstruments.tsx index 36e0b4afb37..0a292625b30 100644 --- a/app/src/organisms/ProtocolSetupInstruments/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/ProtocolSetupInstruments.tsx @@ -11,16 +11,16 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { useInstrumentsQuery } from '@opentrons/react-api-client' -import { ODDBackButton } from '../../molecules/ODDBackButton' -import { PipetteRecalibrationODDWarning } from '../../pages/InstrumentsDashboard/PipetteRecalibrationODDWarning' -import { getShowPipetteCalibrationWarning } from '../Devices/utils' -import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { ProtocolInstrumentMountItem } from '../InstrumentMountItem' +import { ODDBackButton } from '../../../../molecules/ODDBackButton' +import { PipetteRecalibrationODDWarning } from '../../../../pages/ODD/InstrumentsDashboard/PipetteRecalibrationODDWarning' +import { getShowPipetteCalibrationWarning } from '../../../Devices/utils' +import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { ProtocolInstrumentMountItem } from '../../../InstrumentMountItem' import type { GripperData, PipetteData } from '@opentrons/api-client' import type { GripperModel } from '@opentrons/shared-data' -import type { SetupScreens } from '../../pages/ProtocolSetup' -import { isGripperInCommands } from '../../resources/protocols/utils' +import type { SetupScreens } from '../types' +import { isGripperInCommands } from '../../../../resources/protocols/utils' export interface ProtocolSetupInstrumentsProps { runId: string diff --git a/app/src/organisms/ProtocolSetupInstruments/__fixtures__/index.ts b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/__fixtures__/index.ts similarity index 100% rename from app/src/organisms/ProtocolSetupInstruments/__fixtures__/index.ts rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/__fixtures__/index.ts diff --git a/app/src/organisms/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx similarity index 83% rename from app/src/organisms/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx index bcfc0ecf1d6..14edc2b89f8 100644 --- a/app/src/organisms/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx @@ -9,18 +9,18 @@ import { useAllPipetteOffsetCalibrationsQuery, } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useIsOEMMode } from '../../../resources/robot-settings/hooks' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { useMostRecentCompletedAnalysis } from '../../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useIsOEMMode } from '../../../../../resources/robot-settings/hooks' import { mockRecentAnalysis } from '../__fixtures__' import { ProtocolSetupInstruments } from '..' vi.mock('@opentrons/react-api-client') vi.mock( - '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' + '../../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) -vi.mock('../../../resources/robot-settings/hooks') +vi.mock('../../../../../resources/robot-settings/hooks') const mockGripperData = { instrumentModel: 'gripper_v1', diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/index.ts b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/index.ts new file mode 100644 index 00000000000..0d5d7d99a4d --- /dev/null +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/index.ts @@ -0,0 +1,2 @@ +export * from './ProtocolSetupInstruments' +export * from './utils' diff --git a/app/src/organisms/ProtocolSetupInstruments/utils.ts b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/utils.ts similarity index 88% rename from app/src/organisms/ProtocolSetupInstruments/utils.ts rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/utils.ts index 1ce77275e74..ee19492827b 100644 --- a/app/src/organisms/ProtocolSetupInstruments/utils.ts +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/utils.ts @@ -1,7 +1,6 @@ import type { CompletedProtocolAnalysis, LoadedPipette, - ProtocolAnalysisOutput, } from '@opentrons/shared-data' import type { GripperData, @@ -9,16 +8,8 @@ import type { PipetteData, } from '@opentrons/api-client' -export function getProtocolUsesGripper( - analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput -): boolean { - return ( - analysis?.commands.some( - c => - c.commandType === 'moveLabware' && c.params.strategy === 'usingGripper' - ) ?? false - ) -} +import { getProtocolUsesGripper } from '../../../../transformations/commands' + export function getAttachedGripper( attachedInstruments: Instruments ): GripperData | null { diff --git a/app/src/organisms/ProtocolSetupLabware/LabwareMapView.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/LabwareMapView.tsx similarity index 91% rename from app/src/organisms/ProtocolSetupLabware/LabwareMapView.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/LabwareMapView.tsx index cdeabfa2a3e..d7003d8a4fa 100644 --- a/app/src/organisms/ProtocolSetupLabware/LabwareMapView.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/LabwareMapView.tsx @@ -7,8 +7,8 @@ import { THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' -import { getStandardDeckViewLayerBlockList } from '../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' -import { getLabwareRenderInfo } from '../Devices/ProtocolRun/utils/getLabwareRenderInfo' +import { getStandardDeckViewLayerBlockList } from '../../../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' +import { getLabwareRenderInfo } from '../../../Devices/ProtocolRun/utils/getLabwareRenderInfo' import type { CompletedProtocolAnalysis, @@ -71,6 +71,8 @@ export function LabwareMapView(props: LabwareMapViewProps): JSX.Element { } : undefined, highlightLabware: true, + highlightShadowLabware: + topLabwareDefinition != null && topLabwareId != null, moduleChildren: null, stacked: topLabwareDefinition != null && topLabwareId != null, } @@ -99,6 +101,7 @@ export function LabwareMapView(props: LabwareMapViewProps): JSX.Element { }, labwareChildren: null, highlight: true, + highlightShadow: isLabwareInStack, stacked: isLabwareInStack, } } diff --git a/app/src/organisms/ProtocolSetupLabware/SingleLabwareModal.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/SingleLabwareModal.tsx similarity index 94% rename from app/src/organisms/ProtocolSetupLabware/SingleLabwareModal.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/SingleLabwareModal.tsx index 2345397d8c7..c50becdfd95 100644 --- a/app/src/organisms/ProtocolSetupLabware/SingleLabwareModal.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/SingleLabwareModal.tsx @@ -14,11 +14,11 @@ import { LegacyStyledText, SPACING, TYPOGRAPHY, - Modal, } from '@opentrons/components' import { getLabwareDisplayName } from '@opentrons/shared-data' -import { getTopPortalEl } from '../../App/portal' +import { getTopPortalEl } from '../../../../App/portal' +import { OddModal } from '../../../../molecules/OddModal' import type { CompletedProtocolAnalysis, @@ -70,7 +70,7 @@ export const SingleLabwareModal = ( const selectedLabwareLocation = selectedLabware?.location return createPortal( - + - , + , getTopPortalEl() ) } diff --git a/app/src/organisms/ProtocolSetupLabware/__fixtures__/index.ts b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__fixtures__/index.ts similarity index 100% rename from app/src/organisms/ProtocolSetupLabware/__fixtures__/index.ts rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__fixtures__/index.ts diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx similarity index 87% rename from app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx index 532440223d2..ac20b68e8bd 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx @@ -10,10 +10,10 @@ import { fixtureTiprack300ul, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { getLabwareRenderInfo } from '../../Devices/ProtocolRun/utils/getLabwareRenderInfo' -import { getStandardDeckViewLayerBlockList } from '../../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { getLabwareRenderInfo } from '../../../../Devices/ProtocolRun/utils/getLabwareRenderInfo' +import { getStandardDeckViewLayerBlockList } from '../../../../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import { mockProtocolModuleInfo } from '../__fixtures__' import { LabwareMapView } from '../LabwareMapView' @@ -25,12 +25,12 @@ import type { ModuleModel, } from '@opentrons/shared-data' -vi.mock('../../Devices/ProtocolRun/utils/getLabwareRenderInfo') +vi.mock('../../../../Devices/ProtocolRun/utils/getLabwareRenderInfo') vi.mock('@opentrons/components/src/hardware-sim/Labware/LabwareRender') vi.mock('@opentrons/components/src/hardware-sim/BaseDeck') vi.mock('@opentrons/shared-data/js/helpers/getSimplestFlexDeckConfig') -vi.mock('../../../resources/deck_configuration/utils') -vi.mock('../../../redux/config') +vi.mock('../../../../../resources/deck_configuration/utils') +vi.mock('../../../../../redux/config') const MOCK_300_UL_TIPRACK_COORDS = [30, 40, 0] diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx similarity index 88% rename from app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx index 99b3c555dd5..174f0c7b72f 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx @@ -13,10 +13,10 @@ import { ot3StandardDeckV5 as ot3StandardDeckDef, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { getProtocolModulesInfo } from '../../Devices/ProtocolRun/utils/getProtocolModulesInfo' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { useMostRecentCompletedAnalysis } from '../../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { getProtocolModulesInfo } from '../../../../Devices/ProtocolRun/utils/getProtocolModulesInfo' import { ProtocolSetupLabware } from '..' import { mockProtocolModuleInfo, @@ -27,7 +27,7 @@ import { mockUseModulesQueryOpening, mockUseModulesQueryUnknown, } from '../__fixtures__' -import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' +import { useNotifyDeckConfigurationQuery } from '../../../../../resources/deck_configuration' import type * as ReactApiClient from '@opentrons/react-api-client' @@ -41,10 +41,10 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { }) vi.mock( - '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' + '../../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) -vi.mock('../../Devices/ProtocolRun/utils/getProtocolModulesInfo') -vi.mock('../../../resources/deck_configuration') +vi.mock('../../../../Devices/ProtocolRun/utils/getProtocolModulesInfo') +vi.mock('../../../../../resources/deck_configuration') const RUN_ID = "otie's run" const mockSetSetupScreen = vi.fn() diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx similarity index 92% rename from app/src/organisms/ProtocolSetupLabware/index.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx index 456358ddfb7..092138b8b2f 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx @@ -35,14 +35,14 @@ import { useModulesQuery, } from '@opentrons/react-api-client' -import { FloatingActionButton, SmallButton } from '../../atoms/buttons' -import { ODDBackButton } from '../../molecules/ODDBackButton' -import { getLabwareSetupItemGroups } from '../../pages/Protocols/utils' -import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configuration' -import { getProtocolModulesInfo } from '../Devices/ProtocolRun/utils/getProtocolModulesInfo' -import { getNestedLabwareInfo } from '../Devices/ProtocolRun/SetupLabware/getNestedLabwareInfo' -import { LabwareStackModal } from '../Devices/ProtocolRun/SetupLabware/LabwareStackModal' -import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { FloatingActionButton, SmallButton } from '../../../../atoms/buttons' +import { ODDBackButton } from '../../../../molecules/ODDBackButton' +import { getLabwareSetupItemGroups } from '../../../../transformations/commands' +import { useNotifyDeckConfigurationQuery } from '../../../../resources/deck_configuration' +import { getProtocolModulesInfo } from '../../../Devices/ProtocolRun/utils/getProtocolModulesInfo' +import { getNestedLabwareInfo } from '../../../Devices/ProtocolRun/SetupLabware/getNestedLabwareInfo' +import { LabwareStackModal } from '../../../Devices/ProtocolRun/SetupLabware/LabwareStackModal' +import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { getAttachedProtocolModuleMatches } from '../ProtocolSetupModulesAndDeck/utils' import { LabwareMapView } from './LabwareMapView' import { SingleLabwareModal } from './SingleLabwareModal' @@ -57,9 +57,9 @@ import type { RunTimeCommand, } from '@opentrons/shared-data' import type { HeaterShakerModule, Modules } from '@opentrons/api-client' -import type { LabwareSetupItem } from '../../pages/Protocols/utils' -import type { SetupScreens } from '../../pages/ProtocolSetup' -import type { NestedLabwareInfo } from '../Devices/ProtocolRun/SetupLabware/getNestedLabwareInfo' +import type { LabwareSetupItem } from '../../../../transformations/commands' +import type { SetupScreens } from '../types' +import type { NestedLabwareInfo } from '../../../Devices/ProtocolRun/SetupLabware/getNestedLabwareInfo' import type { AttachedProtocolModuleMatch } from '../ProtocolSetupModulesAndDeck/utils' const MODULE_REFETCH_INTERVAL_MS = 5000 @@ -130,13 +130,20 @@ export function ProtocolSetupLabware({ const nickName = onDeckItems.find( item => getLabwareDefURI(item.definition) === foundLabware.definitionUri )?.nickName - setSelectedLabware({ - ...labwareDef, - location: foundLabware.location, - nickName: nickName ?? null, - id: labwareId, - }) - setShowLabwareDetailsModal(true) + const location = onDeckItems.find( + item => item.labwareId === foundLabware.id + )?.initialLocation + if (location != null) { + setSelectedLabware({ + ...labwareDef, + location: location, + nickName: nickName ?? null, + id: labwareId, + }) + setShowLabwareDetailsModal(true) + } else { + console.warn('no initial labware location found') + } } } const selectedLabwareIsTopOfStack = mostRecentAnalysis?.commands.some( @@ -176,9 +183,8 @@ export function ProtocolSetupLabware({ ) : ( )} diff --git a/app/src/organisms/ProtocolSetupLiquids/LiquidDetails.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/LiquidDetails.tsx similarity index 94% rename from app/src/organisms/ProtocolSetupLiquids/LiquidDetails.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/LiquidDetails.tsx index e117134ba77..0b1651ad0ae 100644 --- a/app/src/organisms/ProtocolSetupLiquids/LiquidDetails.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/LiquidDetails.tsx @@ -14,9 +14,9 @@ import { WRAP, } from '@opentrons/components' import { MICRO_LITERS } from '@opentrons/shared-data' -import { LiquidsLabwareDetailsModal } from '../Devices/ProtocolRun/SetupLiquids/LiquidsLabwareDetailsModal' -import { getLocationInfoNames } from '../Devices/ProtocolRun/utils/getLocationInfoNames' -import { getVolumePerWell } from '../Devices/ProtocolRun/SetupLiquids/utils' +import { LiquidsLabwareDetailsModal } from '../../../Devices/ProtocolRun/SetupLiquids/LiquidsLabwareDetailsModal' +import { getLocationInfoNames } from '../../../Devices/ProtocolRun/utils/getLocationInfoNames' +import { getVolumePerWell } from '../../../Devices/ProtocolRun/SetupLiquids/utils' import type { LabwareByLiquidId, diff --git a/app/src/organisms/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx similarity index 72% rename from app/src/organisms/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx index e7a7fafc33d..b1e4103a6c0 100644 --- a/app/src/organisms/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx @@ -2,22 +2,24 @@ import * as React from 'react' import { screen, fireEvent } from '@testing-library/react' import { describe, it, beforeEach, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { RUN_ID_1 } from '../../RunTimeControl/__fixtures__' -import { getLocationInfoNames } from '../../Devices/ProtocolRun/utils/getLocationInfoNames' -import { getVolumePerWell } from '../../Devices/ProtocolRun/SetupLiquids/utils' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { RUN_ID_1 } from '../../../../RunTimeControl/__fixtures__' +import { getLocationInfoNames } from '../../../../Devices/ProtocolRun/utils/getLocationInfoNames' +import { getVolumePerWell } from '../../../../Devices/ProtocolRun/SetupLiquids/utils' import { LiquidDetails } from '../LiquidDetails' -import { LiquidsLabwareDetailsModal } from '../../Devices/ProtocolRun/SetupLiquids/LiquidsLabwareDetailsModal' +import { LiquidsLabwareDetailsModal } from '../../../../Devices/ProtocolRun/SetupLiquids/LiquidsLabwareDetailsModal' import { MOCK_LABWARE_INFO_BY_LIQUID_ID, MOCK_PROTOCOL_ANALYSIS, } from '../fixtures' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' -vi.mock('../../Devices/ProtocolRun/SetupLiquids/utils') -vi.mock('../../Devices/ProtocolRun/utils/getLocationInfoNames') -vi.mock('../../Devices/ProtocolRun/SetupLiquids/LiquidsLabwareDetailsModal') +vi.mock('../../../../Devices/ProtocolRun/SetupLiquids/utils') +vi.mock('../../../../Devices/ProtocolRun/utils/getLocationInfoNames') +vi.mock( + '../../../../Devices/ProtocolRun/SetupLiquids/LiquidsLabwareDetailsModal' +) const render = (props: React.ComponentProps) => { return renderWithProviders(, { diff --git a/app/src/organisms/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx similarity index 79% rename from app/src/organisms/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx index b4dd41061e6..e3e77ab3681 100644 --- a/app/src/organisms/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx @@ -7,11 +7,11 @@ import { parseLiquidsInLoadOrder, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { RUN_ID_1 } from '../../RunTimeControl/__fixtures__' -import { getTotalVolumePerLiquidId } from '../../Devices/ProtocolRun/SetupLiquids/utils' -import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { RUN_ID_1 } from '../../../../RunTimeControl/__fixtures__' +import { getTotalVolumePerLiquidId } from '../../../../Devices/ProtocolRun/SetupLiquids/utils' +import { useMostRecentCompletedAnalysis } from '../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { LiquidDetails } from '../LiquidDetails' import { MOCK_LABWARE_INFO_BY_LIQUID_ID, @@ -22,10 +22,10 @@ import { ProtocolSetupLiquids } from '..' import type * as SharedData from '@opentrons/shared-data' -vi.mock('../../Devices/ProtocolRun/SetupLiquids/utils') -vi.mock('../../../atoms/buttons') +vi.mock('../../../../Devices/ProtocolRun/SetupLiquids/utils') +vi.mock('../../../../../atoms/buttons') vi.mock('../LiquidDetails') -vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') +vi.mock('../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('@opentrons/shared-data', async importOriginal => { const actualSharedData = await importOriginal() return { diff --git a/app/src/organisms/ProtocolSetupLiquids/fixtures.ts b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/fixtures.ts similarity index 100% rename from app/src/organisms/ProtocolSetupLiquids/fixtures.ts rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/fixtures.ts diff --git a/app/src/organisms/ProtocolSetupLiquids/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx similarity index 92% rename from app/src/organisms/ProtocolSetupLiquids/index.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx index 2f831950afe..14290caf69b 100644 --- a/app/src/organisms/ProtocolSetupLiquids/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx @@ -19,14 +19,14 @@ import { parseLabwareInfoByLiquidId, parseLiquidsInLoadOrder, } from '@opentrons/shared-data' -import { ODDBackButton } from '../../molecules/ODDBackButton' -import { SmallButton } from '../../atoms/buttons' +import { ODDBackButton } from '../../../../molecules/ODDBackButton' +import { SmallButton } from '../../../../atoms/buttons' -import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { getTotalVolumePerLiquidId } from '../Devices/ProtocolRun/SetupLiquids/utils' +import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { getTotalVolumePerLiquidId } from '../../../Devices/ProtocolRun/SetupLiquids/utils' import { LiquidDetails } from './LiquidDetails' import type { ParsedLiquid, RunTimeCommand } from '@opentrons/shared-data' -import type { SetupScreens } from '../../pages/ProtocolSetup' +import type { SetupScreens } from '../types' export interface ProtocolSetupLiquidsProps { runId: string @@ -65,7 +65,6 @@ export function ProtocolSetupLiquids({ iconName="ot-check" text={t('liquids_confirmed')} type="success" - chipSize="small" /> ) : ( )} diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx similarity index 92% rename from app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx index b8885dd60a1..904ce8513ce 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -22,10 +22,10 @@ import { SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' -import { SmallButton } from '../../atoms/buttons' -import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' -import { getRequiredDeckConfig } from '../../resources/deck_configuration/utils' -import { LocationConflictModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' +import { SmallButton } from '../../../../atoms/buttons' +import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' +import { getRequiredDeckConfig } from '../../../../resources/deck_configuration/utils' +import { LocationConflictModal } from '../../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' import type { CompletedProtocolAnalysis, @@ -34,10 +34,10 @@ import type { DeckDefinition, RobotType, } from '@opentrons/shared-data' -import type { SetupScreens } from '../../pages/ProtocolSetup' -import type { CutoutConfigAndCompatibility } from '../../resources/deck_configuration/hooks' +import type { SetupScreens } from '../types' +import type { CutoutConfigAndCompatibility } from '../../../../resources/deck_configuration/hooks' import { useSelector } from 'react-redux' -import { getLocalRobot } from '../../redux/discovery' +import { getLocalRobot } from '../../../../redux/discovery' interface FixtureTableProps { robotType: RobotType diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx similarity index 90% rename from app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx index bc6313d0626..664369120b4 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx @@ -25,21 +25,21 @@ import { THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { SmallButton } from '../../atoms/buttons' -import { getModulePrepCommands } from '../../organisms/Devices/getModulePrepCommands' -import { getModuleTooHot } from '../../organisms/Devices/getModuleTooHot' -import { useRunCalibrationStatus } from '../../organisms/Devices/hooks' -import { LocationConflictModal } from '../../organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' -import { ModuleWizardFlows } from '../../organisms/ModuleWizardFlows' -import { useToaster } from '../../organisms/ToasterOven' -import { getLocalRobot } from '../../redux/discovery' -import { useChainLiveCommands } from '../../resources/runs' -import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configuration' +import { SmallButton } from '../../../../atoms/buttons' +import { getModulePrepCommands } from '../../../Devices/getModulePrepCommands' +import { getModuleTooHot } from '../../../Devices/getModuleTooHot' +import { useRunCalibrationStatus } from '../../../Devices/hooks' +import { LocationConflictModal } from '../../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' +import { ModuleWizardFlows } from '../../../ModuleWizardFlows' +import { useToaster } from '../../../ToasterOven' +import { getLocalRobot } from '../../../../redux/discovery' +import { useChainLiveCommands } from '../../../../resources/runs' +import { useNotifyDeckConfigurationQuery } from '../../../../resources/deck_configuration' import type { CommandData } from '@opentrons/api-client' import type { CutoutConfig, DeckDefinition } from '@opentrons/shared-data' -import type { ModulePrepCommandsType } from '../../organisms/Devices/getModulePrepCommands' -import type { ProtocolCalibrationStatus } from '../../organisms/Devices/hooks' +import type { ModulePrepCommandsType } from '../../../Devices/getModulePrepCommands' +import type { ProtocolCalibrationStatus } from '../../../Devices/hooks' import type { AttachedProtocolModuleMatch } from './utils' const DECK_CONFIG_REFETCH_INTERVAL = 5000 diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapView.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModulesAndDeckMapView.tsx similarity index 89% rename from app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapView.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModulesAndDeckMapView.tsx index a6c3c6dddaa..355da533ba1 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapView.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModulesAndDeckMapView.tsx @@ -6,8 +6,8 @@ import { getSimplestDeckConfigForProtocol, } from '@opentrons/shared-data' -import { ModuleInfo } from '../Devices/ModuleInfo' -import { getStandardDeckViewLayerBlockList } from '../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' +import { ModuleInfo } from '../../../Devices/ModuleInfo' +import { getStandardDeckViewLayerBlockList } from '../../../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { AttachedProtocolModuleMatch } from './utils' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx similarity index 88% rename from app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx index 60f8f4be6f9..c32330b27e3 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx @@ -16,14 +16,14 @@ import { } from '@opentrons/shared-data' import { RUN_STATUS_STOPPED } from '@opentrons/api-client' -import { getTopPortalEl } from '../../App/portal' -import { FloatingActionButton } from '../../atoms/buttons' -import { InlineNotification } from '../../atoms/InlineNotification' -import { ChildNavigation } from '../../organisms/ChildNavigation' -import { useAttachedModules } from '../../organisms/Devices/hooks' -import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' -import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useRunStatus } from '../RunTimeControl/hooks' +import { getTopPortalEl } from '../../../../App/portal' +import { FloatingActionButton } from '../../../../atoms/buttons' +import { InlineNotification } from '../../../../atoms/InlineNotification' +import { ChildNavigation } from '../../../../organisms/ChildNavigation' +import { useAttachedModules } from '../../../../organisms/Devices/hooks' +import { getProtocolModulesInfo } from '../../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' +import { useMostRecentCompletedAnalysis } from '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useRunStatus } from '../../../RunTimeControl/hooks' import { getAttachedProtocolModuleMatches, getUnmatchedModulesForProtocol, @@ -32,10 +32,10 @@ import { SetupInstructionsModal } from './SetupInstructionsModal' import { FixtureTable } from './FixtureTable' import { ModuleTable } from './ModuleTable' import { ModulesAndDeckMapView } from './ModulesAndDeckMapView' -import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configuration' +import { useNotifyDeckConfigurationQuery } from '../../../../resources/deck_configuration' import type { CutoutId, CutoutFixtureId } from '@opentrons/shared-data' -import type { SetupScreens } from '../../pages/ProtocolSetup' +import type { SetupScreens } from '../types' const ATTACHED_MODULE_POLL_MS = 5000 const DECK_CONFIG_POLL_MS = 5000 diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx similarity index 88% rename from app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx index 569c59f6c72..2c4e141a1db 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx @@ -12,11 +12,11 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' -import { OddModal } from '../../molecules/OddModal' +import { OddModal } from '../../../../molecules/OddModal' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../../molecules/OddModal/types' -import imgSrc from '../../assets/images/on-device-display/setup_instructions_qr_code.png' +import imgSrc from '../../../../assets/images/on-device-display/setup_instructions_qr_code.png' const INSTRUCTIONS_URL = 'support.opentrons.com/s/modules' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx similarity index 82% rename from app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx index c73a5aacd79..6e7769440ea 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx @@ -10,18 +10,18 @@ import { TRASH_BIN_ADAPTER_FIXTURE, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { LocationConflictModal } from '../../../organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' -import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { LocationConflictModal } from '../../../../../organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' +import { useDeckConfigurationCompatibility } from '../../../../../resources/deck_configuration/hooks' import { FixtureTable } from '../FixtureTable' -import { getLocalRobot } from '../../../redux/discovery' -import { mockConnectedRobot } from '../../../redux/discovery/__fixtures__' +import { getLocalRobot } from '../../../../../redux/discovery' +import { mockConnectedRobot } from '../../../../../redux/discovery/__fixtures__' -vi.mock('../../../redux/discovery') -vi.mock('../../../resources/deck_configuration/hooks') +vi.mock('../../../../../redux/discovery') +vi.mock('../../../../../resources/deck_configuration/hooks') vi.mock( - '../../../organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' + '../../../../../organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' ) const mockSetSetupScreen = vi.fn() diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx similarity index 89% rename from app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx index e0551b3f4f3..14ae921ee15 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx @@ -8,18 +8,18 @@ import { getSimplestDeckConfigForProtocol, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' import { ModulesAndDeckMapView } from '../ModulesAndDeckMapView' vi.mock('@opentrons/components/src/hardware-sim/BaseDeck') vi.mock('@opentrons/api-client') vi.mock('@opentrons/shared-data/js/helpers/getSimplestFlexDeckConfig') -vi.mock('../../../redux/config') -vi.mock('../../Devices/hooks') -vi.mock('../../../resources/deck_configuration/utils') -vi.mock('../../Devices/ModuleInfo') -vi.mock('../../Devices/ProtocolRun/utils/getLabwareRenderInfo') +vi.mock('../../../../../redux/config') +vi.mock('../../../../Devices/hooks') +vi.mock('../../../../../resources/deck_configuration/utils') +vi.mock('../../../../Devices/ModuleInfo') +vi.mock('../../../../Devices/ProtocolRun/utils/getLabwareRenderInfo') const mockRunId = 'mockRunId' const PROTOCOL_ANALYSIS = { diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx similarity index 86% rename from app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx index 9f9cec5524d..cb268c18906 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx @@ -11,51 +11,55 @@ import { getDeckDefFromRobotType, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useChainLiveCommands } from '../../../resources/runs' -import { mockRobotSideAnalysis } from '../../../molecules/Command/__fixtures__' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { useChainLiveCommands } from '../../../../../resources/runs' +import { mockRobotSideAnalysis } from '../../../../../molecules/Command/__fixtures__' import { useAttachedModules, useRunCalibrationStatus, -} from '../../Devices/hooks' -import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { getProtocolModulesInfo } from '../../Devices/ProtocolRun/utils/getProtocolModulesInfo' -import { mockApiHeaterShaker } from '../../../redux/modules/__fixtures__' +} from '../../../../Devices/hooks' +import { useMostRecentCompletedAnalysis } from '../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { getProtocolModulesInfo } from '../../../../Devices/ProtocolRun/utils/getProtocolModulesInfo' +import { mockApiHeaterShaker } from '../../../../../redux/modules/__fixtures__' import { mockProtocolModuleInfo } from '../../ProtocolSetupInstruments/__fixtures__' -import { getLocalRobot } from '../../../redux/discovery' -import { mockConnectedRobot } from '../../../redux/discovery/__fixtures__' +import { getLocalRobot } from '../../../../../redux/discovery' +import { mockConnectedRobot } from '../../../../../redux/discovery/__fixtures__' import { getAttachedProtocolModuleMatches, getUnmatchedModulesForProtocol, } from '../utils' -import { LocationConflictModal } from '../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' -import { ModuleWizardFlows } from '../../ModuleWizardFlows' +import { LocationConflictModal } from '../../../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' +import { ModuleWizardFlows } from '../../../../ModuleWizardFlows' import { SetupInstructionsModal } from '../SetupInstructionsModal' import { FixtureTable } from '../FixtureTable' import { ModulesAndDeckMapView } from '../ModulesAndDeckMapView' import { ProtocolSetupModulesAndDeck } from '..' -import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' -import { useRunStatus } from '../../RunTimeControl/hooks' +import { useNotifyDeckConfigurationQuery } from '../../../../../resources/deck_configuration' +import { useRunStatus } from '../../../../RunTimeControl/hooks' import type { CutoutConfig, DeckConfiguration } from '@opentrons/shared-data' import type { UseQueryResult } from 'react-query' -vi.mock('../../../resources/runs') -vi.mock('../../../redux/discovery') -vi.mock('../../../organisms/Devices/hooks') -vi.mock('../../../resources/deck_configuration') +vi.mock('../../../../../resources/runs') +vi.mock('../../../../../redux/discovery') +vi.mock('../../../../../organisms/Devices/hooks') +vi.mock('../../../../../resources/deck_configuration') vi.mock( - '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' + '../../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +) +vi.mock( + '../../../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' ) -vi.mock('../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo') vi.mock('../utils') vi.mock('../SetupInstructionsModal') -vi.mock('../../ModuleWizardFlows') +vi.mock('../../../../ModuleWizardFlows') vi.mock('../FixtureTable') -vi.mock('../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal') +vi.mock( + '../../../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' +) vi.mock('../ModulesAndDeckMapView') -vi.mock('../../RunTimeControl/hooks') +vi.mock('../../../../RunTimeControl/hooks') const ROBOT_NAME = 'otie' const RUN_ID = '1' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx similarity index 92% rename from app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx index 06db135f3f6..65b1949cef2 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, beforeEach, vi } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' import { SetupInstructionsModal } from '../SetupInstructionsModal' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx similarity index 97% rename from app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx index b96d972ca36..852c5f04e11 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx @@ -4,7 +4,7 @@ import { getModuleDef2, } from '@opentrons/shared-data' -import { mockTemperatureModuleGen2 } from '../../../redux/modules/__fixtures__' +import { mockTemperatureModuleGen2 } from '../../../../../redux/modules/__fixtures__' import { getAttachedProtocolModuleMatches, getUnmatchedModulesForProtocol, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/index.ts b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/index.ts new file mode 100644 index 00000000000..7265f1a9b8c --- /dev/null +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/index.ts @@ -0,0 +1,2 @@ +export * from './ProtocolSetupModulesAndDeck' +export { getUnmatchedModulesForProtocol } from './utils' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/utils.ts b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils.ts similarity index 96% rename from app/src/organisms/ProtocolSetupModulesAndDeck/utils.ts rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils.ts index f1b8601dca1..fb4ecb5ad7f 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/utils.ts +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/utils.ts @@ -10,8 +10,8 @@ import { } from '@opentrons/shared-data' import type { DeckConfiguration, RobotType } from '@opentrons/shared-data' -import type { ProtocolModuleInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' -import type { AttachedModule } from '../../redux/modules/types' +import type { ProtocolModuleInfo } from '../../../Devices/ProtocolRun/utils/getProtocolModulesInfo' +import type { AttachedModule } from '../../../../redux/modules/types' export type AttachedProtocolModuleMatch = ProtocolModuleInfo & { attachedModuleMatch: AttachedModule | null diff --git a/app/src/organisms/ProtocolSetupOffsets/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx similarity index 65% rename from app/src/organisms/ProtocolSetupOffsets/index.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx index b0f8b6f78f6..85395e5a085 100644 --- a/app/src/organisms/ProtocolSetupOffsets/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx @@ -1,22 +1,26 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { - Flex, + Chip, + DIRECTION_COLUMN, DIRECTION_ROW, + Flex, + InfoScreen, JUSTIFY_SPACE_BETWEEN, - Chip, + SPACING, + StyledText, } from '@opentrons/components' import type { LabwareOffset } from '@opentrons/api-client' -import { useToaster } from '../../organisms/ToasterOven' -import { ODDBackButton } from '../../molecules/ODDBackButton' -import { FloatingActionButton, SmallButton } from '../../atoms/buttons' -import type { SetupScreens } from '../../pages/ProtocolSetup' -import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { TerseOffsetTable } from '../../organisms/LabwarePositionCheck/ResultsSummary' -import { getLabwareDefinitionsFromCommands } from '../../molecules/Command/utils/getLabwareDefinitionsFromCommands' -import { useNotifyRunQuery } from '../../resources/runs' -import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' +import { useToaster } from '../../../../organisms/ToasterOven' +import { ODDBackButton } from '../../../../molecules/ODDBackButton' +import { FloatingActionButton, SmallButton } from '../../../../atoms/buttons' +import type { SetupScreens } from '../types' +import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { TerseOffsetTable } from '../../../../organisms/LabwarePositionCheck/ResultsSummary' +import { getLabwareDefinitionsFromCommands } from '../../../../molecules/Command/utils/getLabwareDefinitionsFromCommands' +import { useNotifyRunQuery } from '../../../../resources/runs' +import { getLatestCurrentOffsets } from '../../../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' export interface ProtocolSetupOffsetsProps { runId: string @@ -86,25 +90,39 @@ export function ProtocolSetupOffsets({ ) : ( { setIsConfirmed(true) setSetupScreen('prepare to run') }} + buttonCategory="rounded" /> )} - + + {nonIdentityOffsets.length > 0 ? ( + <> + + {t('applied_labware_offset_data')} + + + + ) : ( + + )} + void } export function AnalysisFailedModal({ errors, protocolId, + runId, setShowAnalysisFailedModal, }: AnalysisFailedModalProps): JSX.Element { const { t } = useTranslation('protocol_setup') @@ -35,8 +38,15 @@ export function AnalysisFailedModal({ hasExitIcon: true, } + const { + isLoading: isDismissing, + mutateAsync: dismissCurrentRunAsync, + } = useDismissCurrentRunMutation() + const handleRestartSetup = (): void => { - navigate(protocolId != null ? `/protocols/${protocolId}` : '/protocols') + dismissCurrentRunAsync(runId).then(() => { + navigate(protocolId != null ? `/protocols/${protocolId}` : '/protocols') + }) } return ( @@ -76,6 +86,9 @@ export function AnalysisFailedModal({ onClick={handleRestartSetup} buttonText={t('restart_setup')} buttonType="alert" + iconName={isDismissing ? 'ot-spinner' : null} + iconPlacement="startIcon" + disabled={isDismissing} /> diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseCsvFile.tsx similarity index 97% rename from app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseCsvFile.tsx index 3126f0ffdff..309d3350724 100644 --- a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseCsvFile.tsx @@ -18,8 +18,8 @@ import { } from '@opentrons/components' import { useAllCsvFilesQuery } from '@opentrons/react-api-client' -import { getShellUpdateDataFiles } from '../../redux/shell' -import { ChildNavigation } from '../ChildNavigation' +import { getShellUpdateDataFiles } from '../../../../redux/shell' +import { ChildNavigation } from '../../../ChildNavigation' import { EmptyFile } from './EmptyFile' import type { @@ -29,7 +29,7 @@ import type { import type { CsvFileData } from '@opentrons/api-client' const MAX_CHARS = 52 -const CSV_FILENAME_BREAK_POINT = 46 +const CSV_FILENAME_BREAK_POINT = 42 interface ChooseCsvFileProps { protocolId: string handleGoBack: () => void diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseEnum.tsx similarity index 95% rename from app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseEnum.tsx index 0ad856f5981..a4723edf734 100644 --- a/app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseEnum.tsx @@ -9,8 +9,8 @@ import { RadioButton, TYPOGRAPHY, } from '@opentrons/components' -import { useToaster } from '../ToasterOven' -import { ChildNavigation } from '../ChildNavigation' +import { useToaster } from '../../../ToasterOven' +import { ChildNavigation } from '../../../ChildNavigation' import type { ChoiceParameter } from '@opentrons/shared-data' interface ChooseEnumProps { diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseNumber.tsx similarity index 96% rename from app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseNumber.tsx index ed6918f9aa8..2a2fe6a4dc6 100644 --- a/app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseNumber.tsx @@ -11,9 +11,9 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { useToaster } from '../ToasterOven' -import { ChildNavigation } from '../ChildNavigation' -import { NumericalKeyboard } from '../../atoms/SoftwareKeyboard' +import { useToaster } from '../../../ToasterOven' +import { ChildNavigation } from '../../../ChildNavigation' +import { NumericalKeyboard } from '../../../../atoms/SoftwareKeyboard' import type { NumberParameter } from '@opentrons/shared-data' interface ChooseNumberProps { diff --git a/app/src/organisms/ProtocolSetupParameters/EmptyFile.stories.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/EmptyFile.stories.tsx similarity index 100% rename from app/src/organisms/ProtocolSetupParameters/EmptyFile.stories.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/EmptyFile.stories.tsx diff --git a/app/src/organisms/ProtocolSetupParameters/EmptyFile.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/EmptyFile.tsx similarity index 100% rename from app/src/organisms/ProtocolSetupParameters/EmptyFile.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/EmptyFile.tsx diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx similarity index 97% rename from app/src/organisms/ProtocolSetupParameters/index.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx index 7bdc2da2bcd..56a76db6dcb 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx @@ -22,14 +22,14 @@ import { import { getRunTimeParameterFilesForRun, getRunTimeParameterValuesForRun, -} from '../Devices/utils' -import { ChildNavigation } from '../ChildNavigation' +} from '../../../Devices/utils' +import { ChildNavigation } from '../../../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' import { ChooseEnum } from './ChooseEnum' import { ChooseNumber } from './ChooseNumber' import { ChooseCsvFile } from './ChooseCsvFile' -import { useToaster } from '../ToasterOven' -import { ProtocolSetupStep } from '../../pages/ProtocolSetup' +import { useToaster } from '../../../ToasterOven' +import { ProtocolSetupStep } from '../ProtocolSetupStep' import type { CompletedProtocolAnalysis, ChoiceParameter, @@ -39,7 +39,7 @@ import type { ValueRunTimeParameter, CsvFileParameterFileData, } from '@opentrons/shared-data' -import type { ProtocolSetupStepStatus } from '../../pages/ProtocolSetup' +import type { ProtocolSetupStepStatus } from '../ProtocolSetupStep' import type { FileData, LabwareOffsetCreateData } from '@opentrons/api-client' interface ProtocolSetupParametersProps { diff --git a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ResetValuesModal.stories.tsx similarity index 100% rename from app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ResetValuesModal.stories.tsx diff --git a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ResetValuesModal.tsx similarity index 91% rename from app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ResetValuesModal.tsx index 2ecc6021618..64468e9176e 100644 --- a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ResetValuesModal.tsx @@ -11,11 +11,11 @@ import { LegacyStyledText, } from '@opentrons/components' -import { SmallButton } from '../../atoms/buttons' -import { OddModal } from '../../molecules/OddModal' +import { SmallButton } from '../../../../atoms/buttons' +import { OddModal } from '../../../../molecules/OddModal' import type { RunTimeParameter } from '@opentrons/shared-data' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../../molecules/OddModal/types' interface ResetValuesModalProps { runTimeParametersOverrides: RunTimeParameter[] diff --git a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ViewOnlyParameters.tsx similarity index 84% rename from app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ViewOnlyParameters.tsx index 8ce37030a4e..2339ae7cc88 100644 --- a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -1,5 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' + import { formatRunTimeParameterValue, sortRuntimeParameters, @@ -16,11 +18,11 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' -import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { ChildNavigation } from '../ChildNavigation' -import { useToaster } from '../ToasterOven' +import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { ChildNavigation } from '../../../ChildNavigation' +import { useToaster } from '../../../ToasterOven' -import type { SetupScreens } from '../../pages/ProtocolSetup' +import type { SetupScreens } from '../types' export interface ViewOnlyParametersProps { runId: string @@ -93,7 +95,7 @@ export function ViewOnlyParameters({ flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing8} > - + {formatRunTimeParameterValue(parameter, t)} {parameter.type === 'csv_file' || @@ -114,3 +116,14 @@ export function ViewOnlyParameters({ ) } + +const PARAMETER_VALUE_STYLE = css` + color: ${COLORS.grey60}; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + -webkit-line-clamp: 1; + max-width: 15rem; +` diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx similarity index 70% rename from app/src/organisms/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx index 2f0be95f26d..96b6c0c8a4a 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx @@ -1,16 +1,23 @@ import * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' +import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' +import { useDismissCurrentRunMutation } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' import { AnalysisFailedModal } from '../AnalysisFailedModal' import type { NavigateFunction } from 'react-router-dom' -const mockNavigate = vi.fn() -const PROTOCOL_ID = 'mockId' +const PROTOCOL_ID = 'mockProtocolId' +const RUN_ID = 'mockRunId' const mockSetShowAnalysisFailedModal = vi.fn() +const mockNavigate = vi.fn() +const mockDismissCurrentRunAsync = vi.fn( + () => new Promise(resolve => resolve({})) +) +vi.mock('@opentrons/react-api-client') vi.mock('react-router-dom', async importOriginal => { const reactRouterDom = await importOriginal() return { @@ -28,6 +35,11 @@ const render = (props: React.ComponentProps) => { describe('AnalysisFailedModal', () => { let props: React.ComponentProps + when(vi.mocked(useDismissCurrentRunMutation)) + .calledWith() + .thenReturn({ + mutateAsync: mockDismissCurrentRunAsync, + } as any) beforeEach(() => { props = { errors: [ @@ -35,6 +47,7 @@ describe('AnalysisFailedModal', () => { 'analysis failed reason message 2', ], protocolId: PROTOCOL_ID, + runId: RUN_ID, setShowAnalysisFailedModal: mockSetShowAnalysisFailedModal, } }) @@ -55,15 +68,10 @@ describe('AnalysisFailedModal', () => { expect(mockSetShowAnalysisFailedModal).toHaveBeenCalled() }) - it('should call a mock function when tapping restart setup button', () => { + it('should call mock dismiss current run function when tapping restart setup button', () => { render(props) fireEvent.click(screen.getByText('Restart setup')) - expect(mockNavigate).toHaveBeenCalledWith(`/protocols/${PROTOCOL_ID}`) - }) - - it('should push to protocols dashboard when tapping restart setup button and protocol ID is null', () => { - render({ ...props, protocolId: null }) - fireEvent.click(screen.getByText('Restart setup')) - expect(mockNavigate).toHaveBeenCalledWith('/protocols') + console.log(mockDismissCurrentRunAsync) + expect(mockDismissCurrentRunAsync).toBeCalled() }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx similarity index 92% rename from app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx index 404ee1059f9..4f095a834e3 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx @@ -5,19 +5,19 @@ import { when } from 'vitest-when' import { useAllCsvFilesQuery } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' -import { mockConnectedRobot } from '../../../redux/discovery/__fixtures__' -import { getLocalRobot } from '../../../redux/discovery' -import { getShellUpdateDataFiles } from '../../../redux/shell' +import { i18n } from '../../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { mockConnectedRobot } from '../../../../../redux/discovery/__fixtures__' +import { getLocalRobot } from '../../../../../redux/discovery' +import { getShellUpdateDataFiles } from '../../../../../redux/shell' import { EmptyFile } from '../EmptyFile' import { ChooseCsvFile } from '../ChooseCsvFile' import type { CsvFileParameter } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') -vi.mock('../../../redux/discovery') -vi.mock('../../../redux/shell') +vi.mock('../../../../../redux/discovery') +vi.mock('../../../../../redux/shell') vi.mock('../EmptyFile') const mockHandleGoBack = vi.fn() diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx similarity index 94% rename from app/src/organisms/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx index 2af4dc11a3c..fffc31e5127 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx @@ -3,11 +3,11 @@ import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { COLORS } from '@opentrons/components' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' import { ChooseEnum } from '../ChooseEnum' -vi.mocked('../../ToasterOven') +vi.mocked('../../../../ToasterOven') const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx similarity index 92% rename from app/src/organisms/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx index 1d312bc36a5..56ec3669c8d 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx @@ -2,15 +2,15 @@ import * as React from 'react' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useToaster } from '../../ToasterOven' -import { mockRunTimeParameterData } from '../../../pages/ProtocolDetails/fixtures' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { useToaster } from '../../../../ToasterOven' +import { mockRunTimeParameterData } from '../../../../../pages/ODD/ProtocolDetails/fixtures' import { ChooseNumber } from '../ChooseNumber' import type { NumberParameter } from '@opentrons/shared-data' -vi.mock('../../ToasterOven') +vi.mock('../../../../ToasterOven') const mockHandleGoBack = vi.fn() const mockIntNumberParameterData = mockRunTimeParameterData[5] as NumberParameter diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/EmptyFile.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/EmptyFile.test.tsx similarity index 92% rename from app/src/organisms/ProtocolSetupParameters/__tests__/EmptyFile.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/EmptyFile.test.tsx index 898a38bbdf1..804eb946cfa 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/EmptyFile.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/EmptyFile.test.tsx @@ -11,8 +11,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' import { EmptyFile } from '../EmptyFile' diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx similarity index 94% rename from app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index d423c0aab1e..1b6fe5ae376 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -10,13 +10,13 @@ import { } from '@opentrons/react-api-client' import { COLORS } from '@opentrons/components' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' import { ChooseEnum } from '../ChooseEnum' import { ChooseNumber } from '../ChooseNumber' import { ChooseCsvFile } from '../ChooseCsvFile' -import { mockRunTimeParameterData } from '../../../pages/ProtocolDetails/fixtures' -import { useToaster } from '../../ToasterOven' +import { mockRunTimeParameterData } from '../../../../../pages/ODD/ProtocolDetails/fixtures' +import { useToaster } from '../../../../ToasterOven' import { ProtocolSetupParameters } from '..' import type { NavigateFunction } from 'react-router-dom' @@ -28,10 +28,10 @@ const mockNavigate = vi.fn() vi.mock('../ChooseEnum') vi.mock('../ChooseNumber') vi.mock('../ChooseCsvFile') -vi.mock('../../../redux/config') -vi.mock('../../ToasterOven') +vi.mock('../../../../../redux/config') +vi.mock('../../../../ToasterOven') vi.mock('@opentrons/react-api-client') -vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') +vi.mock('../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('react-router-dom', async importOriginal => { const reactRouterDom = await importOriginal() return { @@ -39,7 +39,7 @@ vi.mock('react-router-dom', async importOriginal => { useNavigate: () => mockNavigate, } }) -vi.mock('../../../redux/config') +vi.mock('../../../../../redux/config') const MOCK_HOST_CONFIG: HostConfig = { hostname: 'MOCK_HOST' } const mockCreateProtocolAnalysis = vi.fn() diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx similarity index 93% rename from app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx index 46659717788..48d232edc5c 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' import { ResetValuesModal } from '../ResetValuesModal' import type { RunTimeParameter } from '@opentrons/shared-data' diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx similarity index 79% rename from app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx index 6e20fe65658..0c38e37e7ee 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx @@ -2,15 +2,15 @@ import * as React from 'react' import { when } from 'vitest-when' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' -import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useToaster } from '../../ToasterOven' -import { mockRunTimeParameterData } from '../../../pages/ProtocolDetails/fixtures' +import { i18n } from '../../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { useMostRecentCompletedAnalysis } from '../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useToaster } from '../../../../ToasterOven' +import { mockRunTimeParameterData } from '../../../../../pages/ODD/ProtocolDetails/fixtures' import { ViewOnlyParameters } from '../ViewOnlyParameters' -vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') -vi.mock('../../ToasterOven') +vi.mock('../../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') +vi.mock('../../../../ToasterOven') const RUN_ID = 'mockId' const render = (props: React.ComponentProps) => { return renderWithProviders(, { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/index.ts b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/index.ts new file mode 100644 index 00000000000..ecca4bd1516 --- /dev/null +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/index.ts @@ -0,0 +1,3 @@ +export * from './ProtocolSetupParameters' +export * from './AnalysisFailedModal' +export * from './ViewOnlyParameters' diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupSkeleton.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx rename to app/src/organisms/ODD/ProtocolSetup/ProtocolSetupSkeleton.tsx diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupStep/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupStep/index.tsx new file mode 100644 index 00000000000..5078d53b861 --- /dev/null +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupStep/index.tsx @@ -0,0 +1,194 @@ +import * as React from 'react' +import { css } from 'styled-components' +import { + Btn, + Flex, + Icon, + ALIGN_CENTER, + BORDERS, + DIRECTION_COLUMN, + SPACING, + JUSTIFY_END, + TEXT_ALIGN_RIGHT, + COLORS, + TYPOGRAPHY, + NO_WRAP, + LegacyStyledText, +} from '@opentrons/components' +import { useToaster } from '../../../ToasterOven' + +export type ProtocolSetupStepStatus = + | 'ready' + | 'not ready' + | 'general' + | 'inform' +interface ProtocolSetupStepProps { + onClickSetupStep: () => void + status: ProtocolSetupStepStatus + title: string + // first line of detail text + detail?: string | null + // clip detail text overflow with ellipsis + clipDetail?: boolean + // second line of detail text + subDetail?: string | null + // disallow click handler, disabled styling + disabled?: boolean + // disallow click handler, don't show CTA icons, allow styling + interactionDisabled?: boolean + // display the reason the setup step is disabled + disabledReason?: string | null + // optional description + description?: string | null + // optional removal of the left icon + hasLeftIcon?: boolean + // optional removal of the right icon + hasRightIcon?: boolean + // optional enlarge the font size + fontSize?: string +} + +export function ProtocolSetupStep({ + onClickSetupStep, + status, + title, + detail, + subDetail, + disabled = false, + clipDetail = false, + interactionDisabled = false, + disabledReason, + description, + hasRightIcon = true, + hasLeftIcon = true, + fontSize = 'p', +}: ProtocolSetupStepProps): JSX.Element { + const isInteractionDisabled = interactionDisabled || disabled + const backgroundColorByStepStatus = { + ready: COLORS.green35, + 'not ready': COLORS.yellow35, + general: COLORS.grey35, + inform: COLORS.grey35, + } + const { makeSnackbar } = useToaster() + + const makeDisabledReasonSnackbar = (): void => { + if (disabledReason != null) { + makeSnackbar(disabledReason) + } + } + + let backgroundColor: string + if (!disabled) { + switch (status) { + case 'general': + backgroundColor = COLORS.blue35 + break + case 'ready': + backgroundColor = COLORS.green40 + break + case 'inform': + backgroundColor = COLORS.grey50 + break + default: + backgroundColor = COLORS.yellow40 + } + } else backgroundColor = '' + + const PUSHED_STATE_STYLE = css` + &:active { + background-color: ${backgroundColor}; + } + ` + + const isToggle = detail === 'On' || detail === 'Off' + + return ( + { + !isInteractionDisabled + ? onClickSetupStep() + : makeDisabledReasonSnackbar() + }} + width="100%" + data-testid={`SetupButton_${title}`} + > + + {status !== 'general' && + !disabled && + status !== 'inform' && + hasLeftIcon ? ( + + ) : null} + + + {title} + + {description != null ? ( + + {description} + + ) : null} + + + + {detail} + {subDetail != null && detail != null ?
: null} + {subDetail} +
+
+ {interactionDisabled || !hasRightIcon ? null : ( + + )} +
+
+ ) +} + +const CLIPPED_TEXT_STYLE = css` + white-space: ${NO_WRAP}; + overflow: hidden; + text-overflow: ellipsis; +` diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetupSkeleton.test.tsx b/app/src/organisms/ODD/ProtocolSetup/__tests__/ProtocolSetupSkeleton.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetupSkeleton.test.tsx rename to app/src/organisms/ODD/ProtocolSetup/__tests__/ProtocolSetupSkeleton.test.tsx diff --git a/app/src/organisms/ODD/ProtocolSetup/index.ts b/app/src/organisms/ODD/ProtocolSetup/index.ts new file mode 100644 index 00000000000..74473b8146b --- /dev/null +++ b/app/src/organisms/ODD/ProtocolSetup/index.ts @@ -0,0 +1,11 @@ +export * from './ProtocolSetupDeckConfiguration' +export * from './ProtocolSetupInstruments' +export * from './ProtocolSetupLabware' +export * from './ProtocolSetupLiquids' +export * from './ProtocolSetupModulesAndDeck' +export * from './ProtocolSetupOffsets' +export * from './ProtocolSetupParameters' +export * from './ProtocolSetupSkeleton' +export * from './ProtocolSetupStep' + +export type * from './types' diff --git a/app/src/organisms/ODD/ProtocolSetup/types.ts b/app/src/organisms/ODD/ProtocolSetup/types.ts new file mode 100644 index 00000000000..01d704da417 --- /dev/null +++ b/app/src/organisms/ODD/ProtocolSetup/types.ts @@ -0,0 +1,9 @@ +export type SetupScreens = + | 'prepare to run' + | 'instruments' + | 'modules' + | 'offsets' + | 'labware' + | 'liquids' + | 'deck configuration' + | 'view only parameters' diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun.tsx b/app/src/organisms/ODD/RobotDashboard/EmptyRecentRun.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun.tsx rename to app/src/organisms/ODD/RobotDashboard/EmptyRecentRun.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx similarity index 90% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx rename to app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx index 2fb5f75efb2..cc735256244 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx @@ -28,17 +28,17 @@ import { RUN_STATUS_SUCCEEDED, } from '@opentrons/api-client' -import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons//constants' +import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons/constants' import { useTrackEvent, ANALYTICS_PROTOCOL_PROCEED_TO_RUN, } from '../../../redux/analytics' import { Skeleton } from '../../../atoms/Skeleton' -import { useMissingProtocolHardware } from '../../../pages/Protocols/hooks' +import { useMissingProtocolHardware } from '../../../transformations/commands' import { useCloneRun } from '../../ProtocolUpload/hooks' import { useRerunnableStatusText } from './hooks' -import type { Run, RunData, RunStatus } from '@opentrons/api-client' +import type { RunData, RunStatus } from '@opentrons/api-client' import type { ProtocolResource } from '@opentrons/shared-data' interface RecentRunProtocolCardProps { @@ -51,7 +51,8 @@ export function RecentRunProtocolCard({ const { data, isLoading } = useProtocolQuery(runData.protocolId ?? null) const protocolData = data?.data ?? null const isProtocolFetching = isLoading - return protocolData == null ? null : ( + return protocolData == null || + protocolData.protocolKind === 'quick-transfer' ? null : ( { - navigate(`runs/${createRunResponse.data.id}/setup`) - } - const { cloneRun } = useCloneRun(runData.id, onResetSuccess) + const { cloneRun } = useCloneRun(runData.id) const [showSpinner, setShowSpinner] = React.useState(false) const protocolName = @@ -143,6 +141,9 @@ export function ProtocolWithLastRun({ navigate(`/protocols/${protocolId}`) } else { cloneRun() + // Navigate to a dummy setup skeleton until TopLevelRedirects routes to the proper setup page. Doing so prevents + // needing to manage complex UI state updates for protocol cards, overzealous dashboard rendering, and potential navigation pitfalls. + navigate('/runs/1234/setup') trackEvent({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, properties: { sourceLocation: 'RecentRunProtocolCard' }, diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCarousel.tsx b/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCarousel.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCarousel.tsx rename to app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCarousel.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing.tsx b/app/src/organisms/ODD/RobotDashboard/ServerInitializing.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing.tsx rename to app/src/organisms/ODD/RobotDashboard/ServerInitializing.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/EmptyRecentRun.test.tsx b/app/src/organisms/ODD/RobotDashboard/__tests__/EmptyRecentRun.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/EmptyRecentRun.test.tsx rename to app/src/organisms/ODD/RobotDashboard/__tests__/EmptyRecentRun.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx similarity index 95% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx rename to app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index 006bf2dc43e..967b44ce21e 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -16,7 +16,7 @@ import { simpleAnalysisFileFixture } from '@opentrons/shared-data' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' import { Skeleton } from '../../../../atoms/Skeleton' -import { useMissingProtocolHardware } from '../../../../pages/Protocols/hooks' +import { useMissingProtocolHardware } from '../../../../transformations/commands' import { useTrackProtocolRunEvent } from '../../../Devices/hooks' import { useTrackEvent, @@ -28,7 +28,7 @@ import { RecentRunProtocolCard } from '../' import { useNotifyAllRunsQuery } from '../../../../resources/runs' import type { NavigateFunction } from 'react-router-dom' -import type { ProtocolHardware } from '../../../../pages/Protocols/hooks' +import type { ProtocolHardware } from '../../../../transformations/commands' const mockNavigate = vi.fn() @@ -42,8 +42,8 @@ vi.mock('react-router-dom', async importOriginal => { vi.mock('@opentrons/react-api-client') vi.mock('../../../../atoms/Skeleton') -vi.mock('../../../../pages/Protocols/hooks') -vi.mock('../../../../pages/ProtocolDetails') +vi.mock('../../../../transformations/commands') +vi.mock('../../../../pages/ODD/ProtocolDetails') vi.mock('../../../../organisms/Devices/hooks') vi.mock('../../../../organisms/RunTimeControl/hooks') vi.mock('../../../../organisms/ProtocolUpload/hooks') @@ -158,9 +158,11 @@ describe('RecentRunProtocolCard', () => { when(useTrackProtocolRunEvent).calledWith(RUN_ID, ROBOT_NAME).thenReturn({ trackProtocolRunEvent: mockTrackProtocolRunEvent, }) - when(useCloneRun) - .calledWith(RUN_ID, expect.anything()) - .thenReturn({ cloneRun: mockCloneRun, isLoading: false }) + vi.mocked(useCloneRun).mockReturnValue({ + cloneRun: mockCloneRun, + isLoadingRun: false, + isCloning: false, + }) }) afterEach(() => { diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx rename to app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/__tests__/useHardwareStatusText.test.tsx b/app/src/organisms/ODD/RobotDashboard/hooks/__tests__/useHardwareStatusText.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/__tests__/useHardwareStatusText.test.tsx rename to app/src/organisms/ODD/RobotDashboard/hooks/__tests__/useHardwareStatusText.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/index.ts b/app/src/organisms/ODD/RobotDashboard/hooks/index.ts similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/index.ts rename to app/src/organisms/ODD/RobotDashboard/hooks/index.ts diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/useHardwareStatusText.ts b/app/src/organisms/ODD/RobotDashboard/hooks/useHardwareStatusText.ts similarity index 94% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/useHardwareStatusText.ts rename to app/src/organisms/ODD/RobotDashboard/hooks/useHardwareStatusText.ts index b9049596d9a..0f285e2c24e 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/useHardwareStatusText.ts +++ b/app/src/organisms/ODD/RobotDashboard/hooks/useHardwareStatusText.ts @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' -import type { ProtocolHardware } from '../../../../pages/Protocols/hooks' +import type { ProtocolHardware } from '../../../../transformations/commands' export function useHardwareStatusText( missingProtocolHardware: ProtocolHardware[], diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/useRerunnableStatusText.ts b/app/src/organisms/ODD/RobotDashboard/hooks/useRerunnableStatusText.ts similarity index 86% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/useRerunnableStatusText.ts rename to app/src/organisms/ODD/RobotDashboard/hooks/useRerunnableStatusText.ts index 77ecd30a5b9..e9d8353f95f 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/useRerunnableStatusText.ts +++ b/app/src/organisms/ODD/RobotDashboard/hooks/useRerunnableStatusText.ts @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' import { useHardwareStatusText } from './useHardwareStatusText' -import type { ProtocolHardware } from '../../../../pages/Protocols/hooks' +import type { ProtocolHardware } from '../../../../transformations/commands' export function useRerunnableStatusText( runOk: boolean, diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/index.ts b/app/src/organisms/ODD/RobotDashboard/index.ts similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RobotDashboard/index.ts rename to app/src/organisms/ODD/RobotDashboard/index.ts diff --git a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/DeviceReset.tsx similarity index 94% rename from app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/DeviceReset.tsx index 6ad9e59987d..0153e40f22d 100644 --- a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/DeviceReset.tsx @@ -15,20 +15,20 @@ import { useConditionalConfirm, } from '@opentrons/components' -import { MediumButton, SmallButton } from '../../atoms/buttons' -import { OddModal } from '../../molecules/OddModal' -import { ChildNavigation } from '../../organisms/ChildNavigation' +import { MediumButton, SmallButton } from '../../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' +import { ChildNavigation } from '../../../organisms/ChildNavigation' import { getResetConfigOptions, fetchResetConfigOptions, resetConfig, -} from '../../redux/robot-admin' -import { useDispatchApiRequest } from '../../redux/robot-api' +} from '../../../redux/robot-admin' +import { useDispatchApiRequest } from '../../../redux/robot-api' -import type { Dispatch, State } from '../../redux/types' -import type { ResetConfigRequest } from '../../redux/robot-admin/types' -import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { Dispatch, State } from '../../../redux/types' +import type { ResetConfigRequest } from '../../../redux/robot-admin/types' +import type { SetSettingOption } from './types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' interface LabelProps { isSelected?: boolean diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx similarity index 93% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx index f76c04fdf31..2ccb0f56acf 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx @@ -15,11 +15,11 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { ChildNavigation } from '../../../organisms/ChildNavigation' -import { getNetworkInterfaces } from '../../../redux/networking' -import { getLocalRobot } from '../../../redux/discovery' +import { ChildNavigation } from '../../../../organisms/ChildNavigation' +import { getNetworkInterfaces } from '../../../../redux/networking' +import { getLocalRobot } from '../../../../redux/discovery' -import type { State } from '../../../redux/types' +import type { State } from '../../../../redux/types' const STRETCH_LIST_STYLE = css` width: 100%; diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx similarity index 94% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx index 980bf9e5991..9bff7c0c26f 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx @@ -14,9 +14,9 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { OddModal } from '../../../molecules/OddModal' +import { OddModal } from '../../../../molecules/OddModal' -import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../../molecules/OddModal/types' interface NetworkDetailsModalProps { setShowNetworkDetailModal: (showNetworkDetailModal: boolean) => void diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx similarity index 86% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx index b30d49098b6..9c83651b76b 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx @@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' -import { ChildNavigation } from '../../../organisms/ChildNavigation' -import { SetWifiSsid } from '../../../organisms/NetworkSettings/SetWifiSsid' +import { ChildNavigation } from '../../../../organisms/ChildNavigation' +import { SetWifiSsid } from '../../NetworkSettings' -import type { SetSettingOption } from '../../../pages/RobotSettingsDashboard' +import type { SetSettingOption } from '../types' interface RobotSettingsJoinOtherNetworkProps { setCurrentOption: SetSettingOption diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx similarity index 85% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx index feee69d2ad1..be9988ef579 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx @@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' -import { ChildNavigation } from '../../../organisms/ChildNavigation' -import { SelectAuthenticationType } from '../../../organisms/NetworkSettings/SelectAuthenticationType' +import { ChildNavigation } from '../../../../organisms/ChildNavigation' +import { SelectAuthenticationType } from '../../NetworkSettings' import type { WifiSecurityType } from '@opentrons/api-client' -import type { SetSettingOption } from '../../../pages/RobotSettingsDashboard' +import type { SetSettingOption } from '../types' interface RobotSettingsSelectAuthenticationTypeProps { handleWifiConnect: () => void diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx similarity index 81% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx index 7dcf8024b74..68fa084b4a7 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx @@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' -import { ChildNavigation } from '../../../organisms/ChildNavigation' -import { SetWifiCred } from '../../../organisms/NetworkSettings/SetWifiCred' +import { ChildNavigation } from '../../../../organisms/ChildNavigation' +import { SetWifiCred } from '../../NetworkSettings/SetWifiCred' -import type { SetSettingOption } from '../../../pages/RobotSettingsDashboard' +import type { SetSettingOption } from '../types' interface RobotSettingsSetWifiCredProps { handleConnect: () => void diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx similarity index 90% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx index 2126b7a2610..d1f05b10894 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx @@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' -import { ChildNavigation } from '../../../organisms/ChildNavigation' +import { ChildNavigation } from '../../../../organisms/ChildNavigation' import { WifiConnectionDetails } from './WifiConnectionDetails' import type { WifiSecurityType } from '@opentrons/api-client' -import type { SetSettingOption } from '../../../pages/RobotSettingsDashboard' +import type { SetSettingOption } from '../types' interface RobotSettingsWifiProps { setSelectedSsid: React.Dispatch> diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifiConnect.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifiConnect.tsx similarity index 84% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifiConnect.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifiConnect.tsx index 40ba28cdd14..4fb0510b5ff 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifiConnect.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifiConnect.tsx @@ -3,15 +3,12 @@ import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN, SPACING } from '@opentrons/components' -import { ChildNavigation } from '../../../organisms/ChildNavigation' -import { - ConnectingNetwork, - FailedToConnect, -} from '../../../organisms/NetworkSettings' -import { FAILURE, PENDING, SUCCESS } from '../../../redux/robot-api' +import { ChildNavigation } from '../../../../organisms/ChildNavigation' +import { ConnectingNetwork, FailedToConnect } from '../../NetworkSettings' +import { FAILURE, PENDING, SUCCESS } from '../../../../redux/robot-api' -import type { SetSettingOption } from '../../../pages/RobotSettingsDashboard' -import type { RequestState } from '../../../redux/robot-api/types' +import type { SetSettingOption } from '../types' +import type { RequestState } from '../../../../redux/robot-api/types' interface RobotSettingsWifiConnectProps { handleConnect: () => void diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx similarity index 95% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx index 4c944458be2..e4ce4db581e 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx @@ -20,12 +20,12 @@ import { import { NetworkDetailsModal } from './NetworkDetailsModal' import { DisplayWifiList } from '../../NetworkSettings' -import { getLocalRobot } from '../../../redux/discovery' -import { getNetworkInterfaces } from '../../../redux/networking' -import { useWifiList } from '../../../resources/networking/hooks' +import { getLocalRobot } from '../../../../redux/discovery' +import { getNetworkInterfaces } from '../../../../redux/networking' +import { useWifiList } from '../../../../resources/networking/hooks' import type { WifiSecurityType } from '@opentrons/api-client' -import type { State } from '../../../redux/types' +import type { State } from '../../../../redux/types' const FETCH_WIFI_LIST_MS = 5000 diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx similarity index 81% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx index a82b17e3455..9f97bddebb3 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx @@ -3,17 +3,17 @@ import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { i18n } from '../../../../i18n' -import { INTERFACE_ETHERNET } from '../../../../redux/networking' -import { getNetworkInterfaces } from '../../../../redux/networking/selectors' -import { renderWithProviders } from '../../../../__testing-utils__' -import { getLocalRobot } from '../../../../redux/discovery' -import { mockConnectedRobot } from '../../../../redux/discovery/__fixtures__' +import { i18n } from '../../../../../i18n' +import { INTERFACE_ETHERNET } from '../../../../../redux/networking' +import { getNetworkInterfaces } from '../../../../../redux/networking/selectors' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { getLocalRobot } from '../../../../../redux/discovery' +import { mockConnectedRobot } from '../../../../../redux/discovery/__fixtures__' import { EthernetConnectionDetails } from '../EthernetConnectionDetails' -vi.mock('../../../../redux/discovery') -vi.mock('../../../../redux/discovery/selectors') -vi.mock('../../../../redux/networking/selectors') +vi.mock('../../../../../redux/discovery') +vi.mock('../../../../../redux/discovery/selectors') +vi.mock('../../../../../redux/networking/selectors') const render = ( props: React.ComponentProps diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx similarity index 95% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx index 6333bec9b81..1c7766156ee 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx @@ -3,8 +3,8 @@ import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { i18n } from '../../../../i18n' -import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' import { NetworkDetailsModal } from '../NetworkDetailsModal' const mockFn = vi.fn() diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx similarity index 85% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx index 5d27163d300..c0af9d3e2d4 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx @@ -4,19 +4,19 @@ import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { i18n } from '../../../../i18n' -import { renderWithProviders } from '../../../../__testing-utils__' -import { getLocalRobot } from '../../../../redux/discovery' -import { useWifiList } from '../../../../resources/networking/hooks' +import { i18n } from '../../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { getLocalRobot } from '../../../../../redux/discovery' +import { useWifiList } from '../../../../../resources/networking/hooks' import { WifiConnectionDetails } from '../WifiConnectionDetails' import { EthernetConnectionDetails } from '../EthernetConnectionDetails' import { NetworkSettings } from '..' -import type { DiscoveredRobot } from '../../../../redux/discovery/types' -import type { WifiNetwork } from '../../../../redux/networking/types' +import type { DiscoveredRobot } from '../../../../../redux/discovery/types' +import type { WifiNetwork } from '../../../../../redux/networking/types' -vi.mock('../../../../redux/discovery') -vi.mock('../../../../resources/networking/hooks') +vi.mock('../../../../../redux/discovery') +vi.mock('../../../../../resources/networking/hooks') vi.mock('../WifiConnectionDetails') vi.mock('../EthernetConnectionDetails') diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx similarity index 87% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx index 02da87250d3..aa2ca76572b 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx @@ -4,17 +4,17 @@ import { when } from 'vitest-when' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { i18n } from '../../../../i18n' -import { renderWithProviders } from '../../../../__testing-utils__' -import { getLocalRobot } from '../../../../redux/discovery' -import * as Networking from '../../../../redux/networking' +import { i18n } from '../../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { getLocalRobot } from '../../../../../redux/discovery' +import * as Networking from '../../../../../redux/networking' import { NetworkDetailsModal } from '../NetworkDetailsModal' import { WifiConnectionDetails } from '../WifiConnectionDetails' import type * as Dom from 'react-router-dom' -import type { State } from '../../../../redux/types' +import type { State } from '../../../../../redux/types' -vi.mock('../../../../redux/discovery') -vi.mock('../../../../redux/networking') +vi.mock('../../../../../redux/discovery') +vi.mock('../../../../../redux/networking') vi.mock('../NetworkDetailsModal') const mockNavigate = vi.fn() diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/index.tsx similarity index 95% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/index.tsx index 0eda42e2c4b..dfe066aeb54 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/index.tsx @@ -17,11 +17,11 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { ChildNavigation } from '../../../organisms/ChildNavigation' +import { ChildNavigation } from '../../../../organisms/ChildNavigation' import type { IconName, ChipType } from '@opentrons/components' -import type { NetworkConnection } from '../../../resources/networking/hooks/useNetworkConnection' -import type { SetSettingOption } from '../../../pages/RobotSettingsDashboard' +import type { NetworkConnection } from '../../../../resources/networking/hooks/useNetworkConnection' +import type { SetSettingOption } from '../types' export type ConnectionType = 'wifi' | 'ethernet' | 'usb' diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/OnOffToggle.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/OnOffToggle.tsx new file mode 100644 index 00000000000..565a8fb0798 --- /dev/null +++ b/app/src/organisms/ODD/RobotSettingsDashboard/OnOffToggle.tsx @@ -0,0 +1,30 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + ALIGN_CENTER, + BORDERS, + COLORS, + DIRECTION_ROW, + Flex, + LegacyStyledText, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' + +export function OnOffToggle(props: { isOn: boolean }): JSX.Element { + const { t } = useTranslation('shared') + return ( + + + {props.isOn ? t('on') : t('off')} + + + ) +} diff --git a/app/src/organisms/RobotSettingsDashboard/Privacy.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/Privacy.tsx similarity index 81% rename from app/src/organisms/RobotSettingsDashboard/Privacy.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/Privacy.tsx index 51a096af3a3..117b57d8cc9 100644 --- a/app/src/organisms/RobotSettingsDashboard/Privacy.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/Privacy.tsx @@ -10,16 +10,16 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { ChildNavigation } from '../../organisms/ChildNavigation' -import { RobotSettingButton } from '../../pages/RobotSettingsDashboard/RobotSettingButton' -import { OnOffToggle } from '../../pages/RobotSettingsDashboard/RobotSettingsList' +import { ChildNavigation } from '../../../organisms/ChildNavigation' +import { RobotSettingButton } from './RobotSettingButton' +import { OnOffToggle } from './OnOffToggle' import { getAnalyticsOptedIn, toggleAnalyticsOptedIn, -} from '../../redux/analytics' +} from '../../../redux/analytics' -import type { Dispatch } from '../../redux/types' -import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' +import type { Dispatch } from '../../../redux/types' +import type { SetSettingOption } from './types' interface PrivacyProps { robotName: string diff --git a/app/src/organisms/RobotSettingsDashboard/RobotName.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/RobotName.tsx similarity index 91% rename from app/src/organisms/RobotSettingsDashboard/RobotName.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/RobotName.tsx index 40c30ae8877..cb321fdaf95 100644 --- a/app/src/organisms/RobotSettingsDashboard/RobotName.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/RobotName.tsx @@ -10,7 +10,7 @@ import { LegacyStyledText, } from '@opentrons/components' -import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' +import type { SetSettingOption } from './types' interface RobotNameProps { setCurrentOption: SetSettingOption diff --git a/app/src/pages/RobotSettingsDashboard/RobotSettingButton.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/RobotSettingButton.tsx similarity index 100% rename from app/src/pages/RobotSettingsDashboard/RobotSettingButton.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/RobotSettingButton.tsx diff --git a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/RobotSystemVersion.tsx similarity index 92% rename from app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/RobotSystemVersion.tsx index 61e36d7ca18..254d45b829d 100644 --- a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/RobotSystemVersion.tsx @@ -13,12 +13,12 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { MediumButton } from '../../atoms/buttons' -import { ChildNavigation } from '../../organisms/ChildNavigation' +import { MediumButton } from '../../../atoms/buttons' +import { ChildNavigation } from '../../../organisms/ChildNavigation' import { RobotSystemVersionModal } from './RobotSystemVersionModal' -import type { RobotUpdateInfo } from '../../redux/robot-update/types' -import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' +import type { RobotUpdateInfo } from '../../../redux/robot-update/types' +import type { SetSettingOption } from './types' const GITHUB_URL = 'https://github.com/Opentrons/opentrons/releases' diff --git a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/RobotSystemVersionModal.tsx similarity index 84% rename from app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/RobotSystemVersionModal.tsx index 028ca40b1cc..407ff8cb8dd 100644 --- a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/RobotSystemVersionModal.tsx @@ -10,12 +10,12 @@ import { SPACING, } from '@opentrons/components' -import { SmallButton } from '../../atoms/buttons' -import { InlineNotification } from '../../atoms/InlineNotification' -import { ReleaseNotes } from '../../molecules/ReleaseNotes' -import { OddModal } from '../../molecules/OddModal' +import { SmallButton } from '../../../atoms/buttons' +import { InlineNotification } from '../../../atoms/InlineNotification' +import { ReleaseNotes } from '../../../molecules/ReleaseNotes' +import { OddModal } from '../../../molecules/OddModal' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' interface RobotSystemVersionModalProps { version: string diff --git a/app/src/organisms/RobotSettingsDashboard/TextSize.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/TextSize.tsx similarity index 97% rename from app/src/organisms/RobotSettingsDashboard/TextSize.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/TextSize.tsx index dd0bf31910e..ad71888349f 100644 --- a/app/src/organisms/RobotSettingsDashboard/TextSize.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/TextSize.tsx @@ -18,7 +18,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' +import type { SetSettingOption } from './types' interface RectProps { isActive: boolean diff --git a/app/src/organisms/RobotSettingsDashboard/TouchScreenSleep.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx similarity index 89% rename from app/src/organisms/RobotSettingsDashboard/TouchScreenSleep.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx index d6c6c33b2cd..627c9842547 100644 --- a/app/src/organisms/RobotSettingsDashboard/TouchScreenSleep.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx @@ -9,15 +9,15 @@ import { RadioButton, } from '@opentrons/components' -import { ChildNavigation } from '../../organisms/ChildNavigation' +import { ChildNavigation } from '../../../organisms/ChildNavigation' import { getOnDeviceDisplaySettings, updateConfigValue, -} from '../../redux/config' -import { SLEEP_NEVER_MS } from '../../App/constants' +} from '../../../redux/config' +import { SLEEP_NEVER_MS } from '../../../App/constants' -import type { Dispatch } from '../../redux/types' -import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' +import type { Dispatch } from '../../../redux/types' +import type { SetSettingOption } from './types' const SLEEP_TIME_MS = 60 * 1000 // 1 min diff --git a/app/src/organisms/RobotSettingsDashboard/TouchscreenBrightness.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/TouchscreenBrightness.tsx similarity index 91% rename from app/src/organisms/RobotSettingsDashboard/TouchscreenBrightness.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/TouchscreenBrightness.tsx index 7939620f250..6f466ea7f95 100644 --- a/app/src/organisms/RobotSettingsDashboard/TouchscreenBrightness.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/TouchscreenBrightness.tsx @@ -16,15 +16,15 @@ import { SPACING, } from '@opentrons/components' -import { ChildNavigation } from '../../organisms/ChildNavigation' +import { ChildNavigation } from '../../../organisms/ChildNavigation' import { getOnDeviceDisplaySettings, updateConfigValue, -} from '../../redux/config' +} from '../../../redux/config' -import type { Dispatch } from '../../redux/types' -import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' -import { IconButton } from '../../atoms/buttons/IconButton' +import type { Dispatch } from '../../../redux/types' +import type { SetSettingOption } from './types' +import { IconButton } from '../../../atoms/buttons/IconButton' interface BrightnessTileProps { isActive: boolean diff --git a/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx similarity index 96% rename from app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx index 5c3bd7f8737..d1fa185bb32 100644 --- a/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx @@ -13,15 +13,15 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { ChildNavigation } from '../../organisms/ChildNavigation' +import { ChildNavigation } from '../../../organisms/ChildNavigation' import { getDevtoolsEnabled, getUpdateChannel, getUpdateChannelOptions, updateConfigValue, -} from '../../redux/config' +} from '../../../redux/config' -import type { Dispatch } from '../../redux/types' +import type { Dispatch } from '../../../redux/types' interface LabelProps { isSelected?: boolean diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx similarity index 93% rename from app/src/organisms/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx index 2d6fd56f066..d6473d83e85 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx @@ -2,17 +2,20 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' -import { getResetConfigOptions, resetConfig } from '../../../redux/robot-admin' -import { useDispatchApiRequest } from '../../../redux/robot-api' +import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { + getResetConfigOptions, + resetConfig, +} from '../../../../redux/robot-admin' +import { useDispatchApiRequest } from '../../../../redux/robot-api' import { DeviceReset } from '../DeviceReset' -import type { DispatchApiRequestType } from '../../../redux/robot-api' +import type { DispatchApiRequestType } from '../../../../redux/robot-api' -vi.mock('../../../redux/robot-admin') -vi.mock('../../../redux/robot-api') +vi.mock('../../../../redux/robot-admin') +vi.mock('../../../../redux/robot-api') const mockResetConfigOptions = [ { diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/Privacy.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/Privacy.test.tsx similarity index 78% rename from app/src/organisms/RobotSettingsDashboard/__tests__/Privacy.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/__tests__/Privacy.test.tsx index 6e1a12878e3..551b04c8acb 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/Privacy.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/Privacy.test.tsx @@ -2,15 +2,15 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, describe, beforeEach, afterEach, expect, it } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { toggleAnalyticsOptedIn } from '../../../redux/analytics' -import { getRobotSettings } from '../../../redux/robot-settings' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { toggleAnalyticsOptedIn } from '../../../../redux/analytics' +import { getRobotSettings } from '../../../../redux/robot-settings' import { Privacy } from '../Privacy' -vi.mock('../../../redux/analytics') -vi.mock('../../../redux/robot-settings') +vi.mock('../../../../redux/analytics') +vi.mock('../../../../redux/robot-settings') const render = (props: React.ComponentProps) => { return renderWithProviders(, { diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx similarity index 93% rename from app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx index c7ddba35831..8665ed09405 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx @@ -4,12 +4,12 @@ import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' import { RobotSystemVersion } from '../RobotSystemVersion' import { RobotSystemVersionModal } from '../RobotSystemVersionModal' -vi.mock('../../../redux/shell') +vi.mock('../../../../redux/shell') vi.mock('../RobotSystemVersionModal') const mockBack = vi.fn() diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx similarity index 94% rename from app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx index 887b36c332b..9eaa93aabf1 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx @@ -3,8 +3,8 @@ import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' import { RobotSystemVersionModal } from '../RobotSystemVersionModal' import type * as Dom from 'react-router-dom' diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/TextSize.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TextSize.test.tsx similarity index 91% rename from app/src/organisms/RobotSettingsDashboard/__tests__/TextSize.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TextSize.test.tsx index bbe8ccba0d7..ca555bc096d 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/TextSize.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TextSize.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' import { TextSize } from '../TextSize' const mockFunc = vi.fn() diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx similarity index 86% rename from app/src/organisms/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx index c27c2fd112b..65bd35ce0cd 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx @@ -1,12 +1,12 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' -import { i18n } from '../../../i18n' -import { updateConfigValue } from '../../../redux/config' +import { i18n } from '../../../../i18n' +import { updateConfigValue } from '../../../../redux/config' import { TouchScreenSleep } from '../TouchScreenSleep' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') // Note (kj:06/28/2023) this line is to avoid causing errors for scrollIntoView window.HTMLElement.prototype.scrollIntoView = vi.fn() diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx similarity index 92% rename from app/src/organisms/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx index dce842b1691..a3d20baf2cf 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx @@ -2,15 +2,15 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { getOnDeviceDisplaySettings, updateConfigValue, -} from '../../../redux/config' -import { renderWithProviders } from '../../../__testing-utils__' +} from '../../../../redux/config' +import { renderWithProviders } from '../../../../__testing-utils__' import { TouchscreenBrightness } from '../TouchscreenBrightness' -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') const mockFunc = vi.fn() diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx similarity index 92% rename from app/src/organisms/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx rename to app/src/organisms/ODD/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx index 7ed9db3fc0f..cb1a703a0e3 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx @@ -3,17 +3,17 @@ import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { getDevtoolsEnabled, getUpdateChannelOptions, updateConfigValue, -} from '../../../redux/config' -import { renderWithProviders } from '../../../__testing-utils__' +} from '../../../../redux/config' +import { renderWithProviders } from '../../../../__testing-utils__' import { UpdateChannel } from '../UpdateChannel' -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') const mockChannelOptions = [ { diff --git a/app/src/organisms/RobotSettingsDashboard/index.ts b/app/src/organisms/ODD/RobotSettingsDashboard/index.ts similarity index 86% rename from app/src/organisms/RobotSettingsDashboard/index.ts rename to app/src/organisms/ODD/RobotSettingsDashboard/index.ts index ba05950ff24..30933095135 100644 --- a/app/src/organisms/RobotSettingsDashboard/index.ts +++ b/app/src/organisms/ODD/RobotSettingsDashboard/index.ts @@ -12,3 +12,6 @@ export * from './TextSize' export * from './TouchscreenBrightness' export * from './TouchScreenSleep' export * from './UpdateChannel' +export * from './RobotSettingButton' +export * from './OnOffToggle' +export type * from './types' diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/types.ts b/app/src/organisms/ODD/RobotSettingsDashboard/types.ts new file mode 100644 index 00000000000..231d26c837b --- /dev/null +++ b/app/src/organisms/ODD/RobotSettingsDashboard/types.ts @@ -0,0 +1,21 @@ +/** + * a set of screen options for the robot settings dashboard page + */ +export type SettingOption = + | 'NetworkSettings' + | 'RobotName' + | 'RobotSystemVersion' + | 'TouchscreenSleep' + | 'TouchscreenBrightness' + | 'TextSize' + | 'Privacy' + | 'DeviceReset' + | 'UpdateChannel' + | 'EthernetConnectionDetails' + | 'RobotSettingsSelectAuthenticationType' + | 'RobotSettingsJoinOtherNetwork' + | 'RobotSettingsSetWifiCred' + | 'RobotSettingsWifi' + | 'RobotSettingsWifiConnect' + +export type SetSettingOption = (option: SettingOption | null) => void diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal.tsx b/app/src/organisms/ODD/RunningProtocol/CancelingRunModal.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal.tsx rename to app/src/organisms/ODD/RunningProtocol/CancelingRunModal.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx b/app/src/organisms/ODD/RunningProtocol/ConfirmCancelRunModal.tsx similarity index 97% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx rename to app/src/organisms/ODD/RunningProtocol/ConfirmCancelRunModal.tsx index 0a858bf0dfb..c79f7930df7 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx +++ b/app/src/organisms/ODD/RunningProtocol/ConfirmCancelRunModal.tsx @@ -55,8 +55,8 @@ export function ConfirmCancelRunModal({ dismissCurrentRun, isLoading: isDismissing, } = useDismissCurrentRunMutation({ - onSuccess: () => { - if (isQuickTransfer && !isActiveRun) { + onSettled: () => { + if (isQuickTransfer) { deleteRun(runId) } }, @@ -87,8 +87,8 @@ export function ConfirmCancelRunModal({ React.useEffect(() => { if (runStatus === RUN_STATUS_STOPPED) { trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_ACTION.CANCEL }) - dismissCurrentRun(runId) if (!isActiveRun) { + dismissCurrentRun(runId) if (isQuickTransfer && protocolId != null) { navigate(`/quick-transfer/${protocolId}`) } else if (isQuickTransfer) { diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx b/app/src/organisms/ODD/RunningProtocol/CurrentRunningProtocolCommand.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx rename to app/src/organisms/ODD/RunningProtocol/CurrentRunningProtocolCommand.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/PlayPauseButton.tsx b/app/src/organisms/ODD/RunningProtocol/PlayPauseButton.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/PlayPauseButton.tsx rename to app/src/organisms/ODD/RunningProtocol/PlayPauseButton.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx b/app/src/organisms/ODD/RunningProtocol/RunFailedModal.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx rename to app/src/organisms/ODD/RunningProtocol/RunFailedModal.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx b/app/src/organisms/ODD/RunningProtocol/RunningProtocolCommandList.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx rename to app/src/organisms/ODD/RunningProtocol/RunningProtocolCommandList.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolSkeleton.tsx b/app/src/organisms/ODD/RunningProtocol/RunningProtocolSkeleton.tsx similarity index 98% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolSkeleton.tsx rename to app/src/organisms/ODD/RunningProtocol/RunningProtocolSkeleton.tsx index 9947735583a..edee7492427 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolSkeleton.tsx +++ b/app/src/organisms/ODD/RunningProtocol/RunningProtocolSkeleton.tsx @@ -14,7 +14,7 @@ import { PlayPauseButton } from './PlayPauseButton' import { StopButton } from './StopButton' import { Skeleton } from '../../../atoms/Skeleton' -import type { ScreenOption } from '../../../pages/RunningProtocol' +import type { ScreenOption } from '../../../pages/ODD/RunningProtocol' const CURRENT_RUNNING_PROTOCOL_COMMAND_SIZE = '99rem' // CurrentRunningProtocolCommand screen const RUNNING_PROTOCOL_COMMAND_LIST_SIZE = '389rem' // RunningProtocolCommandList screen diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/StopButton.tsx b/app/src/organisms/ODD/RunningProtocol/StopButton.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/StopButton.tsx rename to app/src/organisms/ODD/RunningProtocol/StopButton.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/CancelingRunModal.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/CancelingRunModal.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/CancelingRunModal.test.tsx rename to app/src/organisms/ODD/RunningProtocol/__tests__/CancelingRunModal.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx similarity index 94% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx rename to app/src/organisms/ODD/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx index 358436283aa..b3934d3303c 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx +++ b/app/src/organisms/ODD/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx @@ -138,15 +138,7 @@ describe('ConfirmCancelRunModal', () => { expect(mockStopRun).toHaveBeenCalled() }) - it('when run is stopped, the run is dismissed and the modal closes', () => { - when(useRunStatus).calledWith(RUN_ID).thenReturn(RUN_STATUS_STOPPED) - render(props) - - expect(mockDismissCurrentRun).toHaveBeenCalled() - expect(mockTrackProtocolRunEvent).toHaveBeenCalled() - }) - - it('when run is stopped, the run is dismissed and the modal closes - in prepare to run', () => { + it('when run is stopped, the run is dismissed and the modal closes if the run is not yet active', () => { props = { ...props, isActiveRun: false, @@ -156,9 +148,9 @@ describe('ConfirmCancelRunModal', () => { expect(mockDismissCurrentRun).toHaveBeenCalled() expect(mockTrackProtocolRunEvent).toHaveBeenCalled() - expect(mockNavigate).toHaveBeenCalledWith('/protocols') }) - it('when quick transfer run is stopped, the run is dismissed and you return to quick transfer', () => { + + it('when quick transfer run is stopped, the run is dismissed and the modal closes if the run is not yet active', () => { props = { ...props, isActiveRun: false, @@ -168,6 +160,15 @@ describe('ConfirmCancelRunModal', () => { render(props) expect(mockDismissCurrentRun).toHaveBeenCalled() + expect(mockTrackProtocolRunEvent).toHaveBeenCalled() expect(mockNavigate).toHaveBeenCalledWith('/quick-transfer') }) + + it('when run is stopped, the run is not dismissed if the run is active', () => { + when(useRunStatus).calledWith(RUN_ID).thenReturn(RUN_STATUS_STOPPED) + render(props) + + expect(mockDismissCurrentRun).not.toHaveBeenCalled() + expect(mockTrackProtocolRunEvent).toHaveBeenCalled() + }) }) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/CurrentRunningProtocolCommand.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/CurrentRunningProtocolCommand.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/CurrentRunningProtocolCommand.test.tsx rename to app/src/organisms/ODD/RunningProtocol/__tests__/CurrentRunningProtocolCommand.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/RunFailedModal.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx rename to app/src/organisms/ODD/RunningProtocol/__tests__/RunFailedModal.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx rename to app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunningProtocolSkeleton.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolSkeleton.test.tsx similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunningProtocolSkeleton.test.tsx rename to app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolSkeleton.test.tsx diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/index.ts b/app/src/organisms/ODD/RunningProtocol/index.ts similarity index 100% rename from app/src/organisms/OnDeviceDisplay/RunningProtocol/index.ts rename to app/src/organisms/ODD/RunningProtocol/index.ts diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/hooks.test.tsx b/app/src/organisms/ODD/hooks/__tests__/useIsUnboxingFlowOngoing.test.tsx similarity index 96% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/hooks.test.tsx rename to app/src/organisms/ODD/hooks/__tests__/useIsUnboxingFlowOngoing.test.tsx index 223c2d25457..d7c6e4c8db7 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/hooks.test.tsx +++ b/app/src/organisms/ODD/hooks/__tests__/useIsUnboxingFlowOngoing.test.tsx @@ -8,7 +8,7 @@ import { getIsOnDevice, getOnDeviceDisplaySettings, } from '../../../../redux/config' -import { useIsUnboxingFlowOngoing } from '../hooks' +import { useIsUnboxingFlowOngoing } from '../useIsUnboxingFlowOngoing' import type { Store } from 'redux' import type { State } from '../../../../redux/types' diff --git a/app/src/organisms/ODD/hooks/index.ts b/app/src/organisms/ODD/hooks/index.ts new file mode 100644 index 00000000000..9781d731120 --- /dev/null +++ b/app/src/organisms/ODD/hooks/index.ts @@ -0,0 +1 @@ +export * from './useIsUnboxingFlowOngoing' diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/hooks.ts b/app/src/organisms/ODD/hooks/useIsUnboxingFlowOngoing.ts similarity index 100% rename from app/src/organisms/RobotSettingsDashboard/NetworkSettings/hooks.ts rename to app/src/organisms/ODD/hooks/useIsUnboxingFlowOngoing.ts diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/index.ts b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/index.ts deleted file mode 100644 index 763d2d63602..00000000000 --- a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ProtocolSetupSkeleton' diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardStepsForProtocol.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardStepsForProtocol.test.tsx index 1fc4ea2464e..aa4ad467729 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardStepsForProtocol.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/getPipetteWizardStepsForProtocol.test.tsx @@ -17,6 +17,9 @@ const mockPipetteInfo = [ const mockPipettesInProtocolNotEmpty = [ { id: '123', pipetteName: 'p1000_single_flex', mount: 'left' }, ] +const mockPipettesInProtocolOnRight = [ + { id: '123', pipetteName: 'p1000_single_flex', mount: 'right' }, +] const mockPipettesInProtocolMulti = [ { id: '123', pipetteName: 'p1000_multi_flex', mount: 'left' }, ] @@ -113,7 +116,7 @@ describe('getPipetteWizardStepsForProtocol', () => { ).toStrictEqual(mockFlowSteps) }) - it('returns the correct array of info when the attached 96-channel pipette needs to be switched out for single mount', () => { + it('returns the correct array of info when the attached 96-channel pipette needs to be switched out for single mount on left', () => { const mockFlowSteps = [ { section: SECTIONS.BEFORE_BEGINNING, @@ -176,6 +179,69 @@ describe('getPipetteWizardStepsForProtocol', () => { ) ).toStrictEqual(mockFlowSteps) }) + it('returns the correct array of info when the attached 96-channel pipette needs to be switched out for single mount on right', () => { + const mockFlowSteps = [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.DETACH_PIPETTE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.MOUNTING_PLATE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.CARRIAGE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.DETACH, + nextMount: RIGHT, + }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount: RIGHT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.FIRMWARE_UPDATE, + mount: RIGHT, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount: RIGHT, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount: RIGHT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount: RIGHT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount: RIGHT, + flowType: FLOWS.CALIBRATE, + }, + ] as PipetteWizardStep[] + expect( + getPipetteWizardStepsForProtocol( + { left: mock96ChannelAttachedPipetteInformation, right: null }, + mockPipettesInProtocolOnRight as any, + RIGHT + ) + ).toStrictEqual(mockFlowSteps) + }) it('returns the correct array of info when the attached pipette on left mount needs to be switched out for 96-channel', () => { const mockFlowSteps = [ { diff --git a/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts b/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts index 064b65b7f95..c5fa4739161 100644 --- a/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts +++ b/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts @@ -5,415 +5,461 @@ import type { Mount } from '../../redux/pipettes/types' import type { AttachedPipettesFromInstrumentsQuery } from '../Devices/hooks' import type { PipetteWizardStep } from './types' +const calibrateAlreadyAttachedPipetteOn = ( + mount: Mount +): PipetteWizardStep[] => [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount, + flowType: FLOWS.CALIBRATE, + }, + { + section: SECTIONS.ATTACH_PROBE, + mount, + flowType: FLOWS.CALIBRATE, + }, + { + section: SECTIONS.DETACH_PROBE, + mount, + flowType: FLOWS.CALIBRATE, + }, + { section: SECTIONS.RESULTS, mount, flowType: FLOWS.CALIBRATE }, +] + +const detachNinetySixAndAttachSingleMountOn = ( + mount: Mount +): PipetteWizardStep[] => [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.DETACH_PIPETTE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.MOUNTING_PLATE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.CARRIAGE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.DETACH, + nextMount: mount, + }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.FIRMWARE_UPDATE, + mount, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount, + flowType: FLOWS.CALIBRATE, + }, +] + +const detachSingleMountAndAttachSingleMountOn = ( + mount: Mount +): PipetteWizardStep[] => [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.DETACH_PIPETTE, + mount, + flowType: FLOWS.DETACH, + }, + { section: SECTIONS.RESULTS, mount, flowType: FLOWS.DETACH }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.FIRMWARE_UPDATE, + mount, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount, + flowType: FLOWS.CALIBRATE, + }, +] + +const detachTwoSingleMountsAndAttachNinetySix = (): PipetteWizardStep[] => [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.DETACH_PIPETTE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.DETACH, + nextMount: RIGHT, + }, + { + section: SECTIONS.DETACH_PIPETTE, + mount: RIGHT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.RESULTS, + mount: RIGHT, + flowType: FLOWS.DETACH, + nextMount: 'both', + }, + { + section: SECTIONS.CARRIAGE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNTING_PLATE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.FIRMWARE_UPDATE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.CALIBRATE, + }, +] + +const detachSingleMountOnLeftAndAttachNinetySix = (): PipetteWizardStep[] => [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.DETACH_PIPETTE, + mount: LEFT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.DETACH, + nextMount: 'both', + }, + { + section: SECTIONS.CARRIAGE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNTING_PLATE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.FIRMWARE_UPDATE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.CALIBRATE, + }, +] + +const detachSingleMountOnRightAndAttachNinetySix = (): PipetteWizardStep[] => [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount: RIGHT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.DETACH_PIPETTE, + mount: RIGHT, + flowType: FLOWS.DETACH, + }, + { + section: SECTIONS.RESULTS, + mount: RIGHT, + flowType: FLOWS.DETACH, + nextMount: 'both', + }, + { + section: SECTIONS.CARRIAGE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNTING_PLATE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.FIRMWARE_UPDATE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.CALIBRATE, + }, +] + +const fromEmptyGantryAttachNinetySix = (): PipetteWizardStep[] => [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.CARRIAGE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNTING_PLATE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.FIRMWARE_UPDATE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount: LEFT, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount: LEFT, + flowType: FLOWS.CALIBRATE, + }, +] + +const fromEmptyMountAttachSingleMountOn = ( + mount: Mount +): PipetteWizardStep[] => [ + { + section: SECTIONS.BEFORE_BEGINNING, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.MOUNT_PIPETTE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.FIRMWARE_UPDATE, + mount, + flowType: FLOWS.ATTACH, + }, + { section: SECTIONS.RESULTS, mount, flowType: FLOWS.ATTACH }, + { + section: SECTIONS.ATTACH_PROBE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.DETACH_PROBE, + mount, + flowType: FLOWS.ATTACH, + }, + { + section: SECTIONS.RESULTS, + mount, + flowType: FLOWS.CALIBRATE, + }, +] +/** ++-------------+-----------------------------------------------+----------------------------------------------+-----------------------------------------------+ +| requested > |96 |left |right | +| | | | | +| v attached | | | | ++-------------+-----------------------------------------------+----------------------------------------------+-----------------------------------------------+ +| 96 | calibrateAlreadyAttachedPipetteOn(left) | detachNinetySixAndAttachSingleMountOn(left) | detachNinetySixAndAttachSingleMountOn(right) | ++-------------+-----------------------------------------------+----------------------------------------------+-----------------------------------------------+ +| | | calibrateAlreadyAttachedPipetteOn(left) or | fromEmptyMountAttachSingleMountOn(right) | +| left only | detachSingleMountOnLeftAndAttachNinetySix() | detachSingleMountAndAttachSingleMountOn(left)| | +| | | | | ++-------------+-----------------------------------------------+----------------------------------------------+-----------------------------------------------+ +| | | | calibrateAlreadyAttachedPipetteOn(right) or | +| right only | detachSingleMountOnRightAndAttachNinetySix() |fromEmptyMountAttachSingleMountOn(left) | detachSingleMountAndAttachSingleMountOn(right)| +| | | | | ++-------------+-----------------------------------------------+----------------------------------------------+-----------------------------------------------+ +| left and | | calibrateAlreadyAttachedPipetteOn(left) or | calibrateAlreadyAttachedPipetteOn(right) or | +| right | detachTwoSingleMountsAndAttachNinetySix() | detachSingleMountAndAttachSingleMountOn(left)| detachSingleMountAndAttachSingleMountOn(right)| +| | | | | ++-------------+-----------------------------------------------+----------------------------------------------+-----------------------------------------------+ +| | | | | +| nothing | fromEmptyGantryAttachNinetySix() | fromEmptyMountAttachSingleMountOn(left) | fromEmptyMountAttachSingleMountOn(right) | +| | | | | ++-------------+-----------------------------------------------+----------------------------------------------+-----------------------------------------------+ + **/ + export const getPipetteWizardStepsForProtocol = ( attachedPipettes: AttachedPipettesFromInstrumentsQuery, pipetteInfo: LoadedPipette[], mount: Mount ): PipetteWizardStep[] | null => { const requiredPipette = pipetteInfo.find(pipette => pipette.mount === mount) - const nintySixChannelAttached = + const ninetySixChannelAttached = attachedPipettes[LEFT]?.instrumentName === 'p1000_96' + const ninetySixChannelRequested = requiredPipette?.pipetteName === 'p1000_96' - // return empty array if no pipette is required in the protocol if (requiredPipette == null) { + // return empty array if no pipette is required in the protocol return null - // return calibration flow if correct pipette is attached } else if ( requiredPipette?.pipetteName === attachedPipettes[mount]?.instrumentName ) { - return [ - { - section: SECTIONS.BEFORE_BEGINNING, - mount, - flowType: FLOWS.CALIBRATE, - }, - { - section: SECTIONS.ATTACH_PROBE, - mount, - flowType: FLOWS.CALIBRATE, - }, - { - section: SECTIONS.DETACH_PROBE, - mount, - flowType: FLOWS.CALIBRATE, - }, - { section: SECTIONS.RESULTS, mount, flowType: FLOWS.CALIBRATE }, - ] - } else if ( - requiredPipette.pipetteName !== 'p1000_96' && - attachedPipettes[mount] != null - ) { - // 96-channel pipette attached and need to attach single mount pipette - if (nintySixChannelAttached) { - return [ - { - section: SECTIONS.BEFORE_BEGINNING, - mount: LEFT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.DETACH_PIPETTE, - mount: LEFT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.MOUNTING_PLATE, - mount: LEFT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.CARRIAGE, - mount: LEFT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.RESULTS, - mount: LEFT, - flowType: FLOWS.DETACH, - nextMount: mount, - }, - { - section: SECTIONS.MOUNT_PIPETTE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.FIRMWARE_UPDATE, - mount, - flowType: FLOWS.ATTACH, - }, - { section: SECTIONS.RESULTS, mount, flowType: FLOWS.ATTACH }, - { - section: SECTIONS.ATTACH_PROBE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.DETACH_PROBE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.RESULTS, - mount, - flowType: FLOWS.CALIBRATE, - }, - ] - // Single mount pipette attached and need to attach new single mount pipette - } else { - return [ - { - section: SECTIONS.BEFORE_BEGINNING, - mount, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.DETACH_PIPETTE, - mount, - flowType: FLOWS.DETACH, - }, - { section: SECTIONS.RESULTS, mount, flowType: FLOWS.DETACH }, - { - section: SECTIONS.MOUNT_PIPETTE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.FIRMWARE_UPDATE, - mount, - flowType: FLOWS.ATTACH, - }, - { section: SECTIONS.RESULTS, mount, flowType: FLOWS.ATTACH }, - { - section: SECTIONS.ATTACH_PROBE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.DETACH_PROBE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.RESULTS, - mount, - flowType: FLOWS.CALIBRATE, - }, - ] - } - // Single mount pipette attached to both mounts and need to attach 96-channel pipette + // return calibration flow if correct pipette is attached + return calibrateAlreadyAttachedPipetteOn(mount) + } else if (!ninetySixChannelRequested && ninetySixChannelAttached) { + // 96-channel pipette attached and need to attach single mount pipette + return detachNinetySixAndAttachSingleMountOn(mount) + } else if (!ninetySixChannelRequested && attachedPipettes[mount] != null) { + // Single mount pipette attached and need to attach new single mount pipette + return detachSingleMountAndAttachSingleMountOn(mount) } else if ( - requiredPipette.pipetteName === 'p1000_96' && + ninetySixChannelRequested && attachedPipettes[LEFT] != null && attachedPipettes[RIGHT] != null ) { - return [ - { - section: SECTIONS.BEFORE_BEGINNING, - mount: LEFT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.DETACH_PIPETTE, - mount: LEFT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.RESULTS, - mount: LEFT, - flowType: FLOWS.DETACH, - nextMount: RIGHT, - }, - { - section: SECTIONS.DETACH_PIPETTE, - mount: RIGHT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.RESULTS, - mount: RIGHT, - flowType: FLOWS.DETACH, - nextMount: 'both', - }, - { - section: SECTIONS.CARRIAGE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNTING_PLATE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNT_PIPETTE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.FIRMWARE_UPDATE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, - { - section: SECTIONS.ATTACH_PROBE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.DETACH_PROBE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.RESULTS, - mount: LEFT, - flowType: FLOWS.CALIBRATE, - }, - ] - // Single mount pipette attached to left mount and need to attach 96-channel pipette + // Single mount pipette attached to both mounts and need to attach 96-channel pipette + return detachTwoSingleMountsAndAttachNinetySix() } else if ( - requiredPipette.pipetteName === 'p1000_96' && + ninetySixChannelRequested && attachedPipettes[LEFT] != null && attachedPipettes[RIGHT] == null ) { - return [ - { - section: SECTIONS.BEFORE_BEGINNING, - mount: LEFT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.DETACH_PIPETTE, - mount: LEFT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.RESULTS, - mount: LEFT, - flowType: FLOWS.DETACH, - nextMount: 'both', - }, - { - section: SECTIONS.CARRIAGE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNTING_PLATE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNT_PIPETTE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.FIRMWARE_UPDATE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, - { - section: SECTIONS.ATTACH_PROBE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.DETACH_PROBE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.RESULTS, - mount: LEFT, - flowType: FLOWS.CALIBRATE, - }, - ] - // Single mount pipette attached to right mount and need to attach 96-channel pipette + // Single mount pipette attached to left mount and need to attach 96-channel pipette + return detachSingleMountOnLeftAndAttachNinetySix() } else if ( - requiredPipette.pipetteName === 'p1000_96' && + ninetySixChannelRequested && attachedPipettes[LEFT] == null && attachedPipettes[RIGHT] != null ) { - return [ - { - section: SECTIONS.BEFORE_BEGINNING, - mount: RIGHT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.DETACH_PIPETTE, - mount: RIGHT, - flowType: FLOWS.DETACH, - }, - { - section: SECTIONS.RESULTS, - mount: RIGHT, - flowType: FLOWS.DETACH, - nextMount: 'both', - }, - { - section: SECTIONS.CARRIAGE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNTING_PLATE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNT_PIPETTE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.FIRMWARE_UPDATE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, - { - section: SECTIONS.ATTACH_PROBE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.DETACH_PROBE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.RESULTS, - mount: LEFT, - flowType: FLOWS.CALIBRATE, - }, - ] - // if no pipette is attached to gantry + // Single mount pipette attached to right mount and need to attach 96-channel pipette + return detachSingleMountOnRightAndAttachNinetySix() } else { - // Gantry empty and need to attach 96-channel pipette - if (requiredPipette.pipetteName === 'p1000_96') { - return [ - { - section: SECTIONS.BEFORE_BEGINNING, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.CARRIAGE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNTING_PLATE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNT_PIPETTE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.FIRMWARE_UPDATE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { section: SECTIONS.RESULTS, mount: LEFT, flowType: FLOWS.ATTACH }, - { - section: SECTIONS.ATTACH_PROBE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.DETACH_PROBE, - mount: LEFT, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.RESULTS, - mount: LEFT, - flowType: FLOWS.CALIBRATE, - }, - ] - // Gantry empty and need to attach single mount pipette + // if no pipette is attached to gantry + + if (ninetySixChannelRequested) { + // Gantry empty and need to attach 96-channel pipette + return fromEmptyGantryAttachNinetySix() } else { - return [ - { - section: SECTIONS.BEFORE_BEGINNING, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.MOUNT_PIPETTE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.FIRMWARE_UPDATE, - mount, - flowType: FLOWS.ATTACH, - }, - { section: SECTIONS.RESULTS, mount, flowType: FLOWS.ATTACH }, - { - section: SECTIONS.ATTACH_PROBE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.DETACH_PROBE, - mount, - flowType: FLOWS.ATTACH, - }, - { - section: SECTIONS.RESULTS, - mount, - flowType: FLOWS.CALIBRATE, - }, - ] + // Gantry empty and need to attach single mount pipette + return fromEmptyMountAttachSingleMountOn(mount) } } } diff --git a/app/src/organisms/PipetteWizardFlows/index.tsx b/app/src/organisms/PipetteWizardFlows/index.tsx index b94c5d03c6a..846af77c58b 100644 --- a/app/src/organisms/PipetteWizardFlows/index.tsx +++ b/app/src/organisms/PipetteWizardFlows/index.tsx @@ -16,11 +16,11 @@ import { ApiHostProvider, } from '@opentrons/react-api-client' +import { useCreateTargetedMaintenanceRunMutation } from '../../resources/runs' import { - useCreateTargetedMaintenanceRunMutation, useChainMaintenanceCommands, -} from '../../resources/runs' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' + useNotifyCurrentMaintenanceRun, +} from '../../resources/maintenance_runs' import { getTopPortalEl } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' import { FirmwareUpdateModal } from '../FirmwareUpdateModal' @@ -48,6 +48,7 @@ import type { PipetteMount, } from '@opentrons/shared-data' import type { CommandData, HostConfig } from '@opentrons/api-client' +import { RUN_STATUS_FAILED } from '@opentrons/api-client' import type { PipetteWizardFlow, SelectablePipettes } from './types' const RUN_REFETCH_INTERVAL = 5000 @@ -283,13 +284,16 @@ export const PipetteWizardFlows = ( let onExit if (currentStep == null) return null let modalContent: JSX.Element =
UNASSIGNED STEP
- if (isExiting && errorMessage != null) { + if ( + (isExiting && errorMessage != null) || + maintenanceRunData?.data.status === RUN_STATUS_FAILED + ) { modalContent = ( ) } else if (currentStep.section === SECTIONS.BEFORE_BEGINNING) { @@ -398,7 +402,10 @@ export const PipetteWizardFlows = ( let exitWizardButton = onExit if (isCommandMutationLoading || isDeleteLoading) { exitWizardButton = undefined - } else if (errorMessage != null && isExiting) { + } else if ( + (errorMessage != null && isExiting) || + maintenanceRunData?.data.status === RUN_STATUS_FAILED + ) { exitWizardButton = handleClose } else if (showConfirmExit) { exitWizardButton = handleCleanUpAndClose diff --git a/app/src/organisms/ProtocolAnalysisFailure/index.tsx b/app/src/organisms/ProtocolAnalysisFailure/index.tsx index d04faeeb156..72622a6746a 100644 --- a/app/src/organisms/ProtocolAnalysisFailure/index.tsx +++ b/app/src/organisms/ProtocolAnalysisFailure/index.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' import { useTranslation, Trans } from 'react-i18next' +import { css } from 'styled-components' import { ALIGN_CENTER, @@ -95,11 +96,13 @@ export function ProtocolAnalysisFailure( title={t('protocol_analysis_failure')} onClose={handleClickHideDetails} > - {errors.map((error, index) => ( - - {error} - - ))} + + {errors.map((error, index) => ( + + {error} + + ))} + ) } + +const SCROLL_LONG = css` + overflow: auto; + width: inherit; + max-height: 11.75rem; +` diff --git a/app/src/organisms/ProtocolDetails/ProtocolLabwareDetails.tsx b/app/src/organisms/ProtocolDetails/ProtocolLabwareDetails.tsx index a74d33cf76e..59b4590dc7f 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolLabwareDetails.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolLabwareDetails.tsx @@ -23,7 +23,7 @@ import { getTopPortalEl } from '../../App/portal' import { LabwareDetails } from '../LabwareDetails' import type { LoadLabwareRunTimeCommand } from '@opentrons/shared-data' -import type { LabwareDefAndDate } from '../../pages/Labware/hooks' +import type { LabwareDefAndDate } from '../../pages/Desktop/Labware/hooks' interface ProtocolLabwareDetailsProps { requiredLabwareDetails: LoadLabwareRunTimeCommand[] | null @@ -88,7 +88,7 @@ export const ProtocolLabwareDetails = ( ))} ) : ( - + )} ) diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index 191329bbae8..93d4386487f 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -13,9 +13,7 @@ vi.mock('@opentrons/components', async importOriginal => { const actual = await importOriginal() return { ...actual, - NoParameters: vi.fn(() => ( -
No parameters specified in this protocol
- )), + InfoScreen: vi.fn(() =>
mock InfoScreen
), } }) @@ -133,11 +131,11 @@ describe('ProtocolParameters', () => { screen.getByText('Left, Right') }) - it('should render empty display when protocol does not have any parameter', () => { + it('should render InfoScreen component when protocol does not have any parameter', () => { props = { runTimeParameters: [], } render(props) - screen.getByText('No parameters specified in this protocol') + screen.getByText('mock InfoScreen') }) }) diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx index 61687d995fd..db25d7f6228 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx @@ -47,7 +47,7 @@ export function ProtocolParameters({ ) : ( - + )} ) diff --git a/app/src/organisms/ProtocolDetails/__tests__/ProtocolLabwareDetails.test.tsx b/app/src/organisms/ProtocolDetails/__tests__/ProtocolLabwareDetails.test.tsx index 17498a47ec0..d2ba44d60de 100644 --- a/app/src/organisms/ProtocolDetails/__tests__/ProtocolLabwareDetails.test.tsx +++ b/app/src/organisms/ProtocolDetails/__tests__/ProtocolLabwareDetails.test.tsx @@ -1,18 +1,18 @@ import * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi } from 'vitest' -import { InfoScreen } from '@opentrons/components' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { ProtocolLabwareDetails } from '../ProtocolLabwareDetails' import type { LoadLabwareRunTimeCommand } from '@opentrons/shared-data' +import type { InfoScreen } from '@opentrons/components' vi.mock('@opentrons/components', async importOriginal => { const actual = await importOriginal() return { ...actual, - InfoScreen: vi.fn(), + InfoScreen: () =>
mock InfoScreen
, } }) @@ -79,7 +79,6 @@ describe('ProtocolLabwareDetails', () => { props = { requiredLabwareDetails: mockRequiredLabwareDetails, } - vi.mocked(InfoScreen).mockReturnValue(
mock InfoScreen
) }) it('should render an opentrons labware', () => { diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index d9579f68df6..dda6e06d4a4 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -67,7 +67,7 @@ import { getAnalysisStatus, getProtocolDisplayName, } from '../ProtocolsLanding/utils' -import { getProtocolUsesGripper } from '../ProtocolSetupInstruments/utils' +import { getProtocolUsesGripper } from '../../transformations/commands' import { ProtocolOverflowMenu } from '../ProtocolsLanding/ProtocolOverflowMenu' import { ProtocolStats } from './ProtocolStats' import { ProtocolLabwareDetails } from './ProtocolLabwareDetails' @@ -86,6 +86,12 @@ const GRID_STYLE = css` grid-template-columns: 26.6% 26.6% 26.6% 20.2%; ` +const TWO_COL_GRID_STYLE = css` + display: grid; + grid-gap: ${SPACING.spacing24}; + grid-template-columns: 22.5% 77.5%; +` + const ZOOM_ICON_STYLE = css` border-radius: ${BORDERS.borderRadius4}; &:hover { @@ -130,7 +136,9 @@ function MetadataDetails({ flexDirection={DIRECTION_COLUMN} data-testid="ProtocolDetails_description" > - {description} + + {description} + {filteredMetaData.map((item, index) => { return ( @@ -165,9 +173,11 @@ const ReadMoreContent = (props: ReadMoreContentProps): JSX.Element => { : metadata.description return ( - + {isReadMore ? ( - {description.slice(0, 160)} + + {description.slice(0, 160)} + ) : ( - + {analysisStatus === 'loading' diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index 0132adf7298..d15bee033ed 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -15,7 +15,8 @@ import type { Run } from '@opentrons/api-client' interface UseCloneRunResult { cloneRun: () => void - isLoading: boolean + isLoadingRun: boolean + isCloning: boolean } export function useCloneRun( @@ -25,10 +26,9 @@ export function useCloneRun( ): UseCloneRunResult { const host = useHost() const queryClient = useQueryClient() - const { data: runRecord } = useNotifyRunQuery(runId) + const { data: runRecord, isLoading: isLoadingRun } = useNotifyRunQuery(runId) const protocolKey = runRecord?.data.protocolId ?? null - - const { createRun, isLoading } = useCreateRunMutation({ + const { createRun, isLoading: isCloning } = useCreateRunMutation({ onSuccess: response => { const invalidateRuns = queryClient.invalidateQueries([host, 'runs']) const invalidateProtocols = queryClient.invalidateQueries([ @@ -36,10 +36,13 @@ export function useCloneRun( 'protocols', protocolKey, ]) - Promise.all([invalidateRuns, invalidateProtocols]).catch((e: Error) => { - console.error(`error invalidating runs query: ${e.message}`) - }) - if (onSuccessCallback != null) onSuccessCallback(response) + Promise.all([invalidateRuns, invalidateProtocols]) + .then(() => { + onSuccessCallback?.(response) + }) + .catch((e: Error) => { + console.error(`error invalidating runs query: ${e.message}`) + }) }, }) const { createProtocolAnalysis } = useCreateProtocolAnalysisMutation( @@ -77,5 +80,5 @@ export function useCloneRun( } } - return { cloneRun, isLoading } + return { cloneRun, isLoadingRun, isCloning } } diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloseCurrentRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloseCurrentRun.ts index 58512ba1e9d..6c38ed1aae3 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloseCurrentRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloseCurrentRun.ts @@ -23,10 +23,10 @@ export function useCloseCurrentRun(): { ): void => { if (currentRunId != null) { dismissCurrentRun(currentRunId, { - ...options, onError: () => { console.warn('failed to dismiss current') }, + ...options, }) } } diff --git a/app/src/organisms/ProtocolUpload/hooks/useCurrentRunCommands.ts b/app/src/organisms/ProtocolUpload/hooks/useCurrentRunCommands.ts index b6cc00709f9..543d90cb899 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCurrentRunCommands.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCurrentRunCommands.ts @@ -4,11 +4,11 @@ import type { UseQueryOptions } from 'react-query' import type { CommandsData, RunCommandSummary, - GetCommandsParams, + GetRunCommandsParams, } from '@opentrons/api-client' export function useCurrentRunCommands( - params?: GetCommandsParams, + params?: GetRunCommandsParams, options?: UseQueryOptions ): RunCommandSummary[] | null { const currentRunId = useCurrentRunId() diff --git a/app/src/organisms/ProtocolUpload/hooks/useRunCommands.ts b/app/src/organisms/ProtocolUpload/hooks/useRunCommands.ts index 394c8a3eac0..cb3e70296f8 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useRunCommands.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useRunCommands.ts @@ -4,14 +4,14 @@ import type { UseQueryOptions } from 'react-query' import type { CommandsData, RunCommandSummary, - GetCommandsParams, + GetRunCommandsParams, } from '@opentrons/api-client' const REFETCH_INTERVAL = 3000 export function useRunCommands( runId: string | null, - params?: GetCommandsParams, + params?: GetRunCommandsParams, options?: UseQueryOptions ): RunCommandSummary[] | null { const { data: commandsData } = useNotifyAllCommandsQuery(runId, params, { diff --git a/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx b/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx index e4965c7b96c..55e48479245 100644 --- a/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx +++ b/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx @@ -39,7 +39,7 @@ import { InstrumentContainer } from '../../atoms/InstrumentContainer' import { ProtocolOverflowMenu } from './ProtocolOverflowMenu' import { ProtocolAnalysisFailure } from '../ProtocolAnalysisFailure' import { ProtocolStatusBanner } from '../ProtocolStatusBanner' -import { getProtocolUsesGripper } from '../ProtocolSetupInstruments/utils' +import { getProtocolUsesGripper } from '../../transformations/commands' import { ProtocolAnalysisStale } from '../ProtocolAnalysisFailure/ProtocolAnalysisStale' import { getAnalysisStatus, diff --git a/app/src/organisms/ProtocolsLanding/ProtocolOverflowMenu.tsx b/app/src/organisms/ProtocolsLanding/ProtocolOverflowMenu.tsx index a38ae589e0f..7cbf2814833 100644 --- a/app/src/organisms/ProtocolsLanding/ProtocolOverflowMenu.tsx +++ b/app/src/organisms/ProtocolsLanding/ProtocolOverflowMenu.tsx @@ -74,9 +74,7 @@ export function ProtocolOverflowMenu( }, true) const robotType = - mostRecentAnalysis != null && mostRecentAnalysis.errors.length === 0 - ? mostRecentAnalysis?.robotType ?? null - : null + mostRecentAnalysis != null ? mostRecentAnalysis?.robotType ?? null : null const handleClickShowInFolder: React.MouseEventHandler = e => { e.preventDefault() @@ -117,6 +115,7 @@ export function ProtocolOverflowMenu( navigate(`/protocols/${protocolKey}/timeline`) setShowOverflowMenu(prevShowOverflowMenu => !prevShowOverflowMenu) } + return ( void } +const isValidProtocolFileName = (protocolFileName: string): boolean => { + return protocolFileName.endsWith('.py') || protocolFileName.endsWith('.json') +} + export function ProtocolUploadInput( props: UploadInputProps ): JSX.Element | null { @@ -31,17 +37,24 @@ export function ProtocolUploadInput( const dispatch = useDispatch() const logger = useLogger(new URL('', import.meta.url).pathname) const trackEvent = useTrackEvent() + const { makeToast } = useToaster() const handleUpload = (file: File): void => { if (file.path === null) { logger.warn('Failed to upload file, path not found') } - dispatch(addProtocol(file.path)) + if (isValidProtocolFileName(file.name)) { + dispatch(addProtocol(file.path)) + } else { + makeToast(t('incompatible_file_type') as string, ERROR_TOAST, { + closeButton: true, + }) + } + props.onUpload?.() trackEvent({ name: ANALYTICS_IMPORT_PROTOCOL_TO_APP, properties: { protocolFileName: file.name }, }) - props.onUpload?.() } return ( diff --git a/app/src/organisms/QuickTransferFlow/ConfirmExitModal.tsx b/app/src/organisms/QuickTransferFlow/ConfirmExitModal.tsx index c0b0c46e25f..9ea40d081bf 100644 --- a/app/src/organisms/QuickTransferFlow/ConfirmExitModal.tsx +++ b/app/src/organisms/QuickTransferFlow/ConfirmExitModal.tsx @@ -29,7 +29,7 @@ export const ConfirmExitModal = (props: ConfirmExitModalProps): JSX.Element => { > diff --git a/app/src/organisms/QuickTransferFlow/NameQuickTransfer.tsx b/app/src/organisms/QuickTransferFlow/NameQuickTransfer.tsx index 2d7c8ed21a3..7a79306314e 100644 --- a/app/src/organisms/QuickTransferFlow/NameQuickTransfer.tsx +++ b/app/src/organisms/QuickTransferFlow/NameQuickTransfer.tsx @@ -7,6 +7,7 @@ import { DIRECTION_COLUMN, Flex, InputField, + JUSTIFY_CENTER, LegacyStyledText, POSITION_FIXED, SPACING, @@ -14,7 +15,7 @@ import { } from '@opentrons/components' import { getTopPortalEl } from '../../App/portal' -import { AlphanumericKeyboard } from '../../atoms/SoftwareKeyboard' +import { FullKeyboard } from '../../atoms/SoftwareKeyboard' import { ChildNavigation } from '../ChildNavigation' interface NameQuickTransferProps { @@ -44,42 +45,53 @@ export function NameQuickTransfer(props: NameQuickTransferProps): JSX.Element { buttonIsDisabled={name === '' || error != null} /> - - - {t('enter_characters')} - - - {error} - - - { - setName(input) - }} - keyboardRef={keyboardRef} + + + {t('enter_characters')} + + + {error} + + + { + setName(input) + }} + keyboardRef={keyboardRef} + /> + , getTopPortalEl() ) diff --git a/app/src/organisms/QuickTransferFlow/Overview.tsx b/app/src/organisms/QuickTransferFlow/Overview.tsx index 5840e745f77..9b83ecc174f 100644 --- a/app/src/organisms/QuickTransferFlow/Overview.tsx +++ b/app/src/organisms/QuickTransferFlow/Overview.tsx @@ -11,6 +11,7 @@ import { TEXT_ALIGN_RIGHT, TYPOGRAPHY, } from '@opentrons/components' +import { useToaster } from '../ToasterOven' import { CONSOLIDATE, DISTRIBUTE } from './constants' import type { QuickTransferSummaryState } from './types' @@ -22,6 +23,7 @@ interface OverviewProps { export function Overview(props: OverviewProps): JSX.Element | null { const { state } = props const { t } = useTranslation(['quick_transfer', 'shared']) + const { makeSnackbar } = useToaster() let transferCopy = t('volume_per_well') if (state.transferType === CONSOLIDATE) { @@ -29,6 +31,9 @@ export function Overview(props: OverviewProps): JSX.Element | null { } else if (state.transferType === DISTRIBUTE) { transferCopy = t('dispense_volume') } + const onClick = (): void => { + makeSnackbar(t('create_new_to_edit') as string) + } const displayItems = [ { @@ -63,7 +68,7 @@ export function Overview(props: OverviewProps): JSX.Element | null { marginTop="192px" > {displayItems.map(displayItem => ( - + volumeRange.max) - ? t(`value_out_of_range`, { - min: volumeRange.min, - max: volumeRange.max, - }) - : null + let volumeError = null + if (volumeRange.min > volumeRange.max) { + volumeError = t('air_gap_capacity_error') + } else if ( + volume !== null && + (volume < volumeRange.min || volume > volumeRange.max) + ) { + volumeError = t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + } let buttonIsDisabled = false if (currentStep === 2) { @@ -158,13 +163,13 @@ export function AirGap(props: AirGapProps): JSX.Element { width="100%" > {enableAirGapDisplayItems.map(displayItem => ( - ))} @@ -202,6 +207,7 @@ export function AirGap(props: AirGapProps): JSX.Element { > { setVolume(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx index 9b58e356aae..e43ab686481 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' import { @@ -6,7 +7,7 @@ import { SPACING, DIRECTION_COLUMN, POSITION_FIXED, - LargeButton, + RadioButton, COLORS, } from '@opentrons/components' import { @@ -102,14 +103,14 @@ export function BlowOut(props: BlowOutProps): JSX.Element { const enableBlowOutDisplayItems = [ { - value: true, + option: true, description: t('option_enabled'), onClick: () => { setisBlowOutEnabled(true) }, }, { - value: false, + option: false, description: t('option_disabled'), onClick: () => { setisBlowOutEnabled(false) @@ -175,15 +176,13 @@ export function BlowOut(props: BlowOutProps): JSX.Element { width="100%" > {enableBlowOutDisplayItems.map(displayItem => ( - { - setisBlowOutEnabled(displayItem.value) - }} - buttonText={displayItem.description} + isSelected={isBlowOutEnabled === displayItem.option} + onChange={displayItem.onClick} + buttonValue={displayItem.description} + buttonLabel={displayItem.description} + radioButtonType="large" /> ))} @@ -197,19 +196,20 @@ export function BlowOut(props: BlowOutProps): JSX.Element { width="100%" > {blowOutLocationItems.map(blowOutLocationItem => ( - { + onChange={() => { setBlowOutLocation( blowOutLocationItem.location as BlowOutLocation ) }} - buttonText={blowOutLocationItem.description} + buttonValue={blowOutLocationItem.description} + buttonLabel={blowOutLocationItem.description} + radioButtonType="large" /> ))} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx index 60f88eebfb1..c2a1046c807 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx @@ -8,7 +8,7 @@ import { DIRECTION_COLUMN, Flex, InputField, - LargeButton, + RadioButton, POSITION_FIXED, SPACING, } from '@opentrons/components' @@ -180,13 +180,13 @@ export function Delay(props: DelayProps): JSX.Element { width="100%" > {delayEnabledDisplayItems.map(displayItem => ( - ))} @@ -264,6 +264,7 @@ export function Delay(props: DelayProps): JSX.Element { > { setPosition(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx index 766cc5faea1..de7af18592c 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx @@ -142,6 +142,7 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { > { setFlowRate(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx index 75f3a14a6b3..93dd5f46cd4 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx @@ -8,7 +8,7 @@ import { DIRECTION_COLUMN, Flex, InputField, - LargeButton, + RadioButton, POSITION_FIXED, SPACING, } from '@opentrons/components' @@ -160,13 +160,13 @@ export function Mix(props: MixProps): JSX.Element { width="100%" > {enableMixDisplayItems.map(displayItem => ( - ))} @@ -204,6 +204,7 @@ export function Mix(props: MixProps): JSX.Element { > { setMixVolume(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx index 364b442b159..51ea069ba5f 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -8,7 +9,7 @@ import { DIRECTION_COLUMN, Flex, InputField, - LargeButton, + RadioButton, POSITION_FIXED, SPACING, } from '@opentrons/components' @@ -17,7 +18,6 @@ import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configu import { getTopPortalEl } from '../../../App/portal' import { ChildNavigation } from '../../ChildNavigation' import { useBlowOutLocationOptions } from './BlowOut' -import { getVolumeRange } from '../utils' import { ACTIONS } from '../constants' import { i18n } from '../../../i18n' @@ -51,7 +51,11 @@ export function PipettePath(props: PipettePathProps): JSX.Element { const [disposalVolume, setDisposalVolume] = React.useState( state.volume ) - const volumeLimits = getVolumeRange(state) + const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume + const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume + + // this is the max amount of liquid that can be held in the tip at any time + const maxTipCapacity = Math.min(maxPipetteVolume, tipVolume) const allowedPipettePathOptions: Array<{ pathOption: PathOption @@ -59,7 +63,7 @@ export function PipettePath(props: PipettePathProps): JSX.Element { }> = [{ pathOption: 'single', description: t('pipette_path_single') }] if ( state.transferType === 'distribute' && - volumeLimits.max >= state.volume * 3 + maxTipCapacity >= state.volume * 3 ) { // we have the capacity for a multi dispense if we can fit at least 2x the volume per well // for aspiration plus 1x the volume per well for disposal volume @@ -70,7 +74,7 @@ export function PipettePath(props: PipettePathProps): JSX.Element { // for multi aspirate we only need at least 2x the volume per well } else if ( state.transferType === 'consolidate' && - volumeLimits.max >= state.volume * 2 + maxTipCapacity >= state.volume * 2 ) { allowedPipettePathOptions.push({ pathOption: 'multiAspirate', @@ -116,8 +120,8 @@ export function PipettePath(props: PipettePathProps): JSX.Element { ? t('shared:continue') : t('shared:save') - const maxVolumeCapacity = volumeLimits.max - state.volume * 2 - const volumeRange = { min: 1, max: maxVolumeCapacity } + const maxDisposalCapacity = maxTipCapacity - state.volume * 2 + const volumeRange = { min: 1, max: maxDisposalCapacity } const volumeError = disposalVolume !== null && @@ -153,15 +157,15 @@ export function PipettePath(props: PipettePathProps): JSX.Element { width="100%" > {allowedPipettePathOptions.map(option => ( - { + { setSelectedPath(option.pathOption) }} - buttonText={option.description} + buttonValue={option.description} + buttonLabel={option.description} + radioButtonType="large" /> ))} @@ -199,6 +203,7 @@ export function PipettePath(props: PipettePathProps): JSX.Element { > { setDisposalVolume(Number(e)) }} @@ -215,15 +220,18 @@ export function PipettePath(props: PipettePathProps): JSX.Element { width="100%" > {blowOutLocationItems.map(option => ( - { + onChange={() => { setBlowOutLocation(option.location) }} - buttonText={option.description} + buttonValue={option.description} + buttonLabel={option.description} + radioButtonType="large" /> ))} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx index 18e7d100f37..6bd48f51cf3 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx @@ -133,6 +133,7 @@ export function TipPositionEntry(props: TipPositionEntryProps): JSX.Element { > { setTipPosition(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx index 45d48bde67c..9cf8949b897 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx @@ -8,7 +8,7 @@ import { DIRECTION_COLUMN, Flex, InputField, - LargeButton, + RadioButton, POSITION_FIXED, SPACING, } from '@opentrons/components' @@ -152,15 +152,13 @@ export function TouchTip(props: TouchTipProps): JSX.Element { width="100%" > {enableTouchTipDisplayItems.map(displayItem => ( - ))} @@ -198,6 +196,7 @@ export function TouchTip(props: TouchTipProps): JSX.Element { > { setPosition(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/SaveOrRunModal.tsx b/app/src/organisms/QuickTransferFlow/SaveOrRunModal.tsx index 9fd1de9766e..cfcb20835b6 100644 --- a/app/src/organisms/QuickTransferFlow/SaveOrRunModal.tsx +++ b/app/src/organisms/QuickTransferFlow/SaveOrRunModal.tsx @@ -33,13 +33,10 @@ export const SaveOrRunModal = (props: SaveOrRunModalProps): JSX.Element => { > - + {t('save_to_run_later')} diff --git a/app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx b/app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx index 1c3e656c6d4..5ab2e8a19dc 100644 --- a/app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx @@ -17,7 +17,7 @@ import { getCompatibleLabwareByCategory } from './utils' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SmallButton } from '../../atoms/buttons' -import type { LabwareFilter } from '../../pages/Labware/types' +import type { LabwareFilter } from '../../pages/Desktop/Labware/types' import type { QuickTransferWizardState, QuickTransferWizardAction, @@ -117,8 +117,8 @@ export function SelectDestLabware( onChange={() => { setSelectedLabware('source') }} - buttonLabel={t('source_labware_d2')} - buttonValue="source-labware-d2" + buttonLabel={t('source_labware_c2')} + buttonValue="source-labware-c2" subButtonLabel={state.source.metadata.displayName} /> ) : null} diff --git a/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx b/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx index c12930ce17d..63a70549121 100644 --- a/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx @@ -107,9 +107,10 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { const resetButtonProps: React.ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: t('shared:reset'), - onClick: () => { + onClick: (e: React.MouseEvent) => { setIsNumberWellsSelectedError(false) setSelectedWells({}) + e.currentTarget.blur?.() }, } let labwareDefinition = @@ -150,10 +151,11 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { {labwareDefinition != null ? ( diff --git a/app/src/organisms/QuickTransferFlow/SelectSourceLabware.tsx b/app/src/organisms/QuickTransferFlow/SelectSourceLabware.tsx index 9407332351e..f26a10c68e1 100644 --- a/app/src/organisms/QuickTransferFlow/SelectSourceLabware.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectSourceLabware.tsx @@ -17,7 +17,7 @@ import { getCompatibleLabwareByCategory } from './utils' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SmallButton } from '../../atoms/buttons' -import type { LabwareFilter } from '../../pages/Labware/types' +import type { LabwareFilter } from '../../pages/Desktop/Labware/types' import type { QuickTransferWizardState, QuickTransferWizardAction, diff --git a/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx b/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx index 342bb157193..5f95482fe92 100644 --- a/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx @@ -52,8 +52,9 @@ export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element { const resetButtonProps: React.ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: t('shared:reset'), - onClick: () => { + onClick: (e: React.MouseEvent) => { setSelectedWells({}) + e.currentTarget.blur?.() }, } let displayLabwareDefinition = state.source @@ -82,10 +83,11 @@ export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element { {state.source != null && displayLabwareDefinition != null ? ( diff --git a/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx b/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx index 0436ba47f5f..231b89a02bf 100644 --- a/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx +++ b/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx @@ -59,15 +59,21 @@ export function VolumeEntry(props: VolumeEntryProps): JSX.Element { onNext() } } - - const error = + let error = null + if (volumeRange.min > volumeRange.max) { + error = + state.transferType === 'consolidate' + ? t('consolidate_volume_error') + : t('distribute_volume_error') + } else if ( volume !== '' && (volumeAsNumber < volumeRange.min || volumeAsNumber > volumeRange.max) - ? t(`value_out_of_range`, { - min: volumeRange.min, - max: volumeRange.max, - }) - : null + ) { + error = t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + } return ( diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx index a2d2430c268..84e194834d0 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx +++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx @@ -96,7 +96,7 @@ describe('SelectDestLabware', () => { }, }) render(props) - screen.getByText('Source labware in D2') + screen.getByText('Source labware in C2') screen.getByText('source labware name') }) it('enables continue button if you select a labware', () => { @@ -109,7 +109,7 @@ describe('SelectDestLabware', () => { }) const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') expect(continueBtn).toBeDisabled() - const sourceLabware = screen.getByText('Source labware in D2') + const sourceLabware = screen.getByText('Source labware in C2') fireEvent.click(sourceLabware) expect(continueBtn).toBeEnabled() }) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts b/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts index 4937af941d5..0f04ae374c0 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts +++ b/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts @@ -1,8 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, vi, afterEach } from 'vitest' import { getInitialSummaryState } from '../../utils' -import { getVolumeRange } from '../../utils/getVolumeRange' - -vi.mock('../../utils/getVolumeRange') describe('getInitialSummaryState', () => { const props = { @@ -11,6 +8,7 @@ describe('getInitialSummaryState', () => { channels: 1, liquids: { default: { + maxVolume: 100, supportedTips: { t50: { defaultAspirateFlowRate: { @@ -46,9 +44,6 @@ describe('getInitialSummaryState', () => { }, ], } as any - beforeEach(() => { - vi.mocked(getVolumeRange).mockReturnValue({ min: 5, max: 100 }) - }) afterEach(() => { vi.resetAllMocks() }) @@ -125,11 +120,13 @@ describe('getInitialSummaryState', () => { ...props, state: { ...props.state, + volume: 10, transferType: 'distribute', }, }) expect(initialSummaryState).toEqual({ ...props.state, + volume: 10, transferType: 'distribute', aspirateFlowRate: 50, dispenseFlowRate: 75, @@ -142,7 +139,7 @@ describe('getInitialSummaryState', () => { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter', }, - disposalVolume: props.state.volume, + disposalVolume: 10, blowOut: { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter' }, }) }) diff --git a/app/src/organisms/QuickTransferFlow/utils/generateCompatibleLabwareForPipette.ts b/app/src/organisms/QuickTransferFlow/utils/generateCompatibleLabwareForPipette.ts index 3b52d014e93..55c5ead6b30 100644 --- a/app/src/organisms/QuickTransferFlow/utils/generateCompatibleLabwareForPipette.ts +++ b/app/src/organisms/QuickTransferFlow/utils/generateCompatibleLabwareForPipette.ts @@ -1,5 +1,5 @@ import { makeWellSetHelpers, getLabwareDefURI } from '@opentrons/shared-data' -import { getAllDefinitions as getAllLatestDefValues } from '../../../pages/Labware/helpers/definitions' +import { getAllDefinitions as getAllLatestDefValues } from '../../../pages/Desktop/Labware/helpers/definitions' import type { PipetteV2Specs, WellSetHelpers } from '@opentrons/shared-data' diff --git a/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts b/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts index 4f571665b10..517e0aabb0d 100644 --- a/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts +++ b/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts @@ -299,18 +299,23 @@ export function generateQuickTransferArgs( const flowRatesForSupportedTip = quickTransferState.pipette.liquids.default.supportedTips[tipType] const pipetteEntity = Object.values(invariantContext.pipetteEntities)[0] - const labwareEntityValues = Object.values(invariantContext.labwareEntities) - const sourceLabwareEntity = labwareEntityValues.find( - entity => - entity.labwareDefURI === getLabwareDefURI(quickTransferState.source) + + const sourceLabwareId = Object.keys(robotState.labware).find( + labwareId => robotState.labware[labwareId].slot === 'C2' ) + const sourceLabwareEntity = + sourceLabwareId != null + ? invariantContext.labwareEntities[sourceLabwareId] + : undefined let destLabwareEntity = sourceLabwareEntity if (quickTransferState.destination !== 'source') { - destLabwareEntity = labwareEntityValues.find( - entity => - entity.labwareDefURI === - getLabwareDefURI(quickTransferState.destination as LabwareDefinition2) + const destinationLabwareId = Object.keys(robotState.labware).find( + labwareId => robotState.labware[labwareId].slot === 'D2' ) + destLabwareEntity = + destinationLabwareId != null + ? invariantContext.labwareEntities[destinationLabwareId] + : undefined } let nozzles = null diff --git a/app/src/organisms/QuickTransferFlow/utils/getCompatibleLabwareByCategory.ts b/app/src/organisms/QuickTransferFlow/utils/getCompatibleLabwareByCategory.ts index a9bf05fae9e..ed75679cd39 100644 --- a/app/src/organisms/QuickTransferFlow/utils/getCompatibleLabwareByCategory.ts +++ b/app/src/organisms/QuickTransferFlow/utils/getCompatibleLabwareByCategory.ts @@ -6,7 +6,7 @@ import { } from '../constants' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LabwareFilter } from '../../../pages/Labware/types' +import type { LabwareFilter } from '../../../pages/Desktop/Labware/types' export function getCompatibleLabwareByCategory( pipetteChannels: 1 | 8 | 96, diff --git a/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts b/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts index d073dd13894..cb6187a388c 100644 --- a/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts +++ b/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts @@ -3,7 +3,6 @@ import { TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_FIXTURES, } from '@opentrons/shared-data' -import { getVolumeRange } from './' import type { LabwareDefinition2, @@ -43,20 +42,24 @@ export function getInitialSummaryState( const flowRatesForSupportedTip = state.pipette.liquids.default.supportedTips[tipType] - const volumeLimits = getVolumeRange(state) + const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume + const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume + + // this is the max amount of liquid that can be held in the tip at any time + const maxTipCapacity = Math.min(maxPipetteVolume, tipVolume) let path: PathOption = 'single' // for multiDispense the volume capacity must be at least 3x the volume per well // to account for the 1x volume per well disposal volume default if ( state.transferType === 'distribute' && - volumeLimits.max >= state.volume * 3 + maxTipCapacity >= state.volume * 3 ) { path = 'multiDispense' // for multiAspirate the volume capacity must be at least 2x the volume per well } else if ( state.transferType === 'consolidate' && - volumeLimits.max >= state.volume * 2 + maxTipCapacity >= state.volume * 2 ) { path = 'multiAspirate' } diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx index ec3d28a2140..aaa506bd390 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx @@ -214,6 +214,8 @@ export function OverflowMenu({ css={css` border-radius: ${BORDERS.borderRadius8}; `} + disabled={isRunning} + aria-label={`CalibrationOverflowMenu_button_calibrate`} > {t( ot3PipCal == null diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx index f35f4d64997..3bdb82574c5 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx @@ -3,7 +3,7 @@ import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' -import { OT3_PIPETTES } from '@opentrons/shared-data' +import { OT3_PIPETTES, isFlexPipette } from '@opentrons/shared-data' import { useDeleteCalibrationMutation, useAllPipetteOffsetCalibrationsQuery, @@ -27,6 +27,7 @@ import { import { renderWithProviders } from '../../../../__testing-utils__' import { useIsEstopNotDisengaged } from '../../../../resources/devices/hooks/useIsEstopNotDisengaged' import { OverflowMenu } from '../OverflowMenu' + import type { Mount } from '@opentrons/components' const render = ( @@ -51,6 +52,14 @@ vi.mock('file-saver', async importOriginal => { saveAs: vi.fn(), } }) + +vi.mock('@opentrons/shared-data', async () => { + const actual = await vi.importActual('@opentrons/shared-data') + return { + ...actual, + isFlexPipette: vi.fn(), + } +}) vi.mock('@opentrons/react-api-client') vi.mock('../../../../redux/sessions/selectors') vi.mock('../../../../redux/discovery') @@ -113,6 +122,7 @@ describe('OverflowMenu', () => { }, } as any) when(useIsEstopNotDisengaged).calledWith(ROBOT_NAME).thenReturn(false) + vi.mocked(isFlexPipette).mockReturnValue(false) }) it('should render Overflow menu buttons - pipette offset calibrations', () => { @@ -186,6 +196,7 @@ describe('OverflowMenu', () => { ...props, pipetteName: OT3_PIPETTE_NAME, } + vi.mocked(isFlexPipette).mockReturnValue(true) render(props) const button = screen.getByLabelText( 'CalibrationOverflowMenu_button_pipetteOffset' @@ -212,6 +223,7 @@ describe('OverflowMenu', () => { ...props, pipetteName: OT3_PIPETTE_NAME, } + vi.mocked(isFlexPipette).mockReturnValue(true) render(props) const button = screen.getByLabelText( 'CalibrationOverflowMenu_button_pipetteOffset' @@ -302,4 +314,22 @@ describe('OverflowMenu', () => { screen.getByLabelText('CalibrationOverflowMenu_button_pipetteOffset') ).toBeDisabled() }) + + it('should disable the calibration overflow menu option when the run is running', () => { + vi.mocked(useRunStatuses).mockReturnValue({ + ...RUN_STATUSES, + isRunRunning: true, + }) + vi.mocked(isFlexPipette).mockReturnValue(true) + + render(props) + + fireEvent.click( + screen.getByLabelText('CalibrationOverflowMenu_button_pipetteOffset') + ) + + expect( + screen.getByLabelText('CalibrationOverflowMenu_button_calibrate') + ).toBeDisabled() + }) }) diff --git a/app/src/organisms/RunPreview/index.tsx b/app/src/organisms/RunPreview/index.tsx index 9808acbb929..27ed73fedf9 100644 --- a/app/src/organisms/RunPreview/index.tsx +++ b/app/src/organisms/RunPreview/index.tsx @@ -35,7 +35,6 @@ import { useLastRunCommand } from '../Devices/hooks/useLastRunCommand' import type { RunStatus } from '@opentrons/api-client' import type { RobotType } from '@opentrons/shared-data' import type { ViewportListRef } from 'react-viewport-list' - const COLOR_FADE_MS = 500 const LIVE_RUN_COMMANDS_POLL_MS = 3000 // arbitrary large number of commands @@ -51,7 +50,7 @@ export const RunPreviewComponent = ( { runId, jumpedIndex, makeHandleScrollToStep, robotType }: RunPreviewProps, ref: React.ForwardedRef ): JSX.Element | null => { - const { t } = useTranslation('run_details') + const { t } = useTranslation(['run_details', 'protocol_setup']) const robotSideAnalysis = useMostRecentCompletedAnalysis(runId) const runStatus = useRunStatus(runId) const { data: runRecord } = useNotifyRunQuery(runId) @@ -65,10 +64,8 @@ export const RunPreviewComponent = ( isLoading: isRunCommandDataLoading, } = useNotifyAllCommandsAsPreSerializedList( runId, - { cursor: 0, pageLength: MAX_COMMANDS }, + { cursor: 0, pageLength: MAX_COMMANDS, includeFixitCommands: false }, { - staleTime: Infinity, - cacheTime: Infinity, enabled: isRunTerminal, } ) @@ -81,10 +78,15 @@ export const RunPreviewComponent = ( isCurrentCommandVisible, setIsCurrentCommandVisible, ] = React.useState(true) - if (robotSideAnalysis == null) return null + + if (robotSideAnalysis == null) { + return null + } + const commands = isRunTerminal ? commandsFromQuery : robotSideAnalysis.commands + // pass relevant data from run rather than analysis so that CommandText utilities can properly hash the entities' IDs // TODO (nd:05/02/2024, AUTH-380): update name and types for CommandText (and children/utilities) use of analysis. // We should ideally pass only subset of analysis/run data required by these children and utilities @@ -103,7 +105,6 @@ export const RunPreviewComponent = ( commands != null ? commands.findIndex(c => c.key === currentRunCommandKey) : 0 - if (isRunCommandDataLoading || commands == null) { return ( @@ -115,7 +116,7 @@ export const RunPreviewComponent = ( } return commands.length === 0 ? ( - + ) : ( { .calledWith(NON_DETERMINISTIC_RUN_ID) .thenReturn(null) when(useNotifyAllCommandsQuery) - .calledWith(NON_DETERMINISTIC_RUN_ID, { cursor: null, pageLength: 1 }) + .calledWith(NON_DETERMINISTIC_RUN_ID, { + cursor: null, + pageLength: 1, + }) .thenReturn(mockUseAllCommandsResponseNonDeterministic) when(useCommandQuery) .calledWith(NON_DETERMINISTIC_RUN_ID, NON_DETERMINISTIC_COMMAND_KEY) @@ -99,12 +102,12 @@ describe('RunProgressMeter', () => { showModal: false, modalProps: {} as any, }) + vi.mocked(useRunControls).mockReturnValue({ play: vi.fn() } as any) props = { runId: NON_DETERMINISTIC_RUN_ID, robotName: ROBOT_NAME, makeHandleJumpToStep: vi.fn(), - resumeRunHandler: vi.fn(), } }) diff --git a/app/src/organisms/RunProgressMeter/index.tsx b/app/src/organisms/RunProgressMeter/index.tsx index 892a86c4b60..424d2ed5381 100644 --- a/app/src/organisms/RunProgressMeter/index.tsx +++ b/app/src/organisms/RunProgressMeter/index.tsx @@ -29,7 +29,7 @@ import { import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { getModalPortalEl } from '../../App/portal' -import { useRunStatus } from '../RunTimeControl/hooks' +import { useRunControls, useRunStatus } from '../RunTimeControl/hooks' import { InterventionModal, useInterventionModal } from '../InterventionModal' import { ProgressBar } from '../../atoms/ProgressBar' import { useDownloadRunLog, useRobotType } from '../Devices/hooks' @@ -45,13 +45,13 @@ interface RunProgressMeterProps { runId: string robotName: string makeHandleJumpToStep: (index: number) => () => void - resumeRunHandler: () => void } export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element { - const { runId, robotName, makeHandleJumpToStep, resumeRunHandler } = props + const { runId, robotName, makeHandleJumpToStep } = props const { t } = useTranslation('run_details') const robotType = useRobotType(robotName) const runStatus = useRunStatus(runId) + const { play } = useRunControls(runId) const [targetProps, tooltipProps] = useHoverTooltip({ placement: TOOLTIP_LEFT, }) @@ -122,10 +122,7 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element { <> {showIntervention ? createPortal( - , + , getModalPortalEl() ) : null} diff --git a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx index 8107a236383..04bff11ab8f 100644 --- a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx +++ b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx @@ -59,9 +59,11 @@ describe('useRunControls hook', () => { isStopRunActionLoading: false, isResumeRunFromRecoveryActionLoading: false, }) - when(useCloneRun) - .calledWith(mockPausedRun.id, undefined, true) - .thenReturn({ cloneRun: mockCloneRun, isLoading: false }) + when(useCloneRun).calledWith(mockPausedRun.id, undefined, true).thenReturn({ + cloneRun: mockCloneRun, + isCloning: false, + isLoadingRun: false, + }) const { result } = renderHook(() => useRunControls(mockPausedRun.id)) diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index 606e5852f36..f6be73a0706 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -34,6 +34,7 @@ export interface RunControls { isStopRunActionLoading: boolean isResumeRunFromRecoveryActionLoading: boolean isResetRunLoading: boolean + isRunControlLoading: boolean } export function useRunControls( @@ -51,11 +52,11 @@ export function useRunControls( isResumeRunFromRecoveryActionLoading, } = useRunActionMutations(runId as string) - const { cloneRun, isLoading: isResetRunLoading } = useCloneRun( - runId ?? null, - onCloneRunSuccess, - true - ) + const { + cloneRun, + isLoadingRun: isRunControlLoading, + isCloning: isResetRunLoading, + } = useCloneRun(runId ?? null, onCloneRunSuccess, true) return { play: playRun, @@ -67,6 +68,7 @@ export function useRunControls( isPauseRunActionLoading, isStopRunActionLoading, isResumeRunFromRecoveryActionLoading, + isRunControlLoading, isResetRunLoading, } } diff --git a/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx b/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx index cd3ab48d411..31791a278e9 100644 --- a/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx +++ b/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx @@ -24,7 +24,7 @@ import { } from '../../../redux/discovery' import { getValidCustomLabwareFiles } from '../../../redux/custom-labware' import { renderWithProviders } from '../../../__testing-utils__' -import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../../redux/robot-update' import { mockConnectableRobot, mockReachableRobot, @@ -93,11 +93,7 @@ const mockCustomLabwareFile: File = { path: 'fake_custom_labware_path' } as any describe('SendProtocolToFlexSlideout', () => { beforeEach(() => { - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: '', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(false) vi.mocked(getConnectableRobots).mockReturnValue([mockConnectableOT3]) vi.mocked(getUnreachableRobots).mockReturnValue([mockUnreachableOT3]) vi.mocked(getReachableRobots).mockReturnValue([mockReachableOT3]) @@ -233,11 +229,7 @@ describe('SendProtocolToFlexSlideout', () => { }) }) it('if selected robot is on a different version of the software than the app, disable CTA and show link to device details in options', () => { - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) + vi.mocked(useIsRobotOnWrongVersionOfSoftware).mockReturnValue(true) render({ storedProtocolData: storedProtocolDataFixture, onCloseClick: vi.fn(), diff --git a/app/src/organisms/SendProtocolToFlexSlideout/index.tsx b/app/src/organisms/SendProtocolToFlexSlideout/index.tsx index e52b004daad..ed5375c340a 100644 --- a/app/src/organisms/SendProtocolToFlexSlideout/index.tsx +++ b/app/src/organisms/SendProtocolToFlexSlideout/index.tsx @@ -21,7 +21,7 @@ import { useToaster } from '../../organisms/ToasterOven' import { appShellRequestor } from '../../redux/shell/remote' import { OPENTRONS_USB } from '../../redux/discovery' import { getIsProtocolAnalysisInProgress } from '../../redux/protocol-storage' -import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' +import { useIsRobotOnWrongVersionOfSoftware } from '../../redux/robot-update' import { getValidCustomLabwareFiles } from '../../redux/custom-labware' import type { AxiosError } from 'axios' @@ -54,15 +54,10 @@ export function SendProtocolToFlexSlideout( const [selectedRobot, setSelectedRobot] = React.useState(null) - const { autoUpdateAction } = useSelector((state: State) => - getRobotUpdateDisplayInfo(state, selectedRobot?.name ?? '') + const isSelectedRobotOnDifferentSoftwareVersion = useIsRobotOnWrongVersionOfSoftware( + selectedRobot?.name ?? '' ) - const isSelectedRobotOnDifferentSoftwareVersion = [ - 'upgrade', - 'downgrade', - ].includes(autoUpdateAction) - const { eatToast, makeToast } = useToaster() const { mutateAsync: createProtocolAsync } = useCreateProtocolMutation( diff --git a/app/src/organisms/WellSelection/Selection384Wells.tsx b/app/src/organisms/WellSelection/Selection384Wells.tsx index d69cbcb94fe..5db84c16cd9 100644 --- a/app/src/organisms/WellSelection/Selection384Wells.tsx +++ b/app/src/organisms/WellSelection/Selection384Wells.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next' import flatten from 'lodash/flatten' import { - Box, Checkbox, DIRECTION_COLUMN, Flex, @@ -131,16 +130,17 @@ export function Selection384Wells({ ) } return ( - - {labwareRender} + + {labwareRender} {channels === 1 ? ( {i18n.format(t('select_by'), 'capitalize')} - { - setSelectBy('columns') - setLastSelectedIndex(lastSelectedIndex => - lastSelectedIndex != null - ? Math.floor(lastSelectedIndex / ROW_COUNT_384) - : lastSelectedIndex - ) - }} - radioButtonType="small" - /> - { - setSelectBy('wells') - setLastSelectedIndex(lastSelectedIndex => - lastSelectedIndex != null - ? (lastSelectedIndex + 1) * ROW_COUNT_384 - 1 - : lastSelectedIndex - ) - }} - radioButtonType="small" - /> + + { + setSelectBy('columns') + setLastSelectedIndex(lastSelectedIndex => + lastSelectedIndex != null + ? Math.floor(lastSelectedIndex / ROW_COUNT_384) + : lastSelectedIndex + ) + }} + radioButtonType="small" + /> + { + setSelectBy('wells') + setLastSelectedIndex(lastSelectedIndex => + lastSelectedIndex != null + ? (lastSelectedIndex + 1) * ROW_COUNT_384 - 1 + : lastSelectedIndex + ) + }} + radioButtonType="small" + /> + ) } @@ -269,27 +271,29 @@ function StartingWell({ {i18n.format(t('starting_well'), 'capitalize')} - {checkboxWellOptions.map(well => ( - { - if (channels === 96) { - if (startingWellState[well]) { - deselectWells([well]) - } else { - selectWells({ [well]: null }) + + {checkboxWellOptions.map(well => ( + { + if (channels === 96) { + if (startingWellState[well]) { + deselectWells([well]) + } else { + selectWells({ [well]: null }) + } } - } - setStartingWellState(startingWellState => ({ - ...startingWellState, - [well]: !startingWellState[well], - })) - }} - /> - ))} + setStartingWellState(startingWellState => ({ + ...startingWellState, + [well]: !startingWellState[well], + })) + }} + /> + ))} + ) } @@ -321,7 +325,7 @@ function ButtonControls(props: ButtonControlsProps): JSX.Element { 'capitalize' )}
- + => { return renderWithProviders( diff --git a/app/src/pages/AppSettings/__test__/AppSettings.test.tsx b/app/src/pages/Desktop/AppSettings/__test__/AppSettings.test.tsx similarity index 85% rename from app/src/pages/AppSettings/__test__/AppSettings.test.tsx rename to app/src/pages/Desktop/AppSettings/__test__/AppSettings.test.tsx index fecd160e0e2..b4402f9661d 100644 --- a/app/src/pages/AppSettings/__test__/AppSettings.test.tsx +++ b/app/src/pages/Desktop/AppSettings/__test__/AppSettings.test.tsx @@ -3,21 +3,21 @@ import { vi, describe, beforeEach, it, expect, afterEach } from 'vitest' import { Route } from 'react-router' import { MemoryRouter, Routes } from 'react-router-dom' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' -import * as Config from '../../../redux/config' +import { i18n } from '../../../../i18n' +import * as Config from '../../../../redux/config' import { GeneralSettings } from '../GeneralSettings' import { PrivacySettings } from '../PrivacySettings' import { AdvancedSettings } from '../AdvancedSettings' -import { FeatureFlags } from '../../../organisms/AppSettings/FeatureFlags' +import { FeatureFlags } from '../../../../organisms/AppSettings/FeatureFlags' import { AppSettings } from '..' -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') vi.mock('../GeneralSettings') vi.mock('../PrivacySettings') vi.mock('../AdvancedSettings') -vi.mock('../../../organisms/AppSettings/FeatureFlags') +vi.mock('../../../../organisms/AppSettings/FeatureFlags') const render = (path = '/'): ReturnType => { return renderWithProviders( diff --git a/app/src/pages/AppSettings/__test__/GeneralSettings.test.tsx b/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx similarity index 88% rename from app/src/pages/AppSettings/__test__/GeneralSettings.test.tsx rename to app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx index 7a3d7196858..7e9727bac34 100644 --- a/app/src/pages/AppSettings/__test__/GeneralSettings.test.tsx +++ b/app/src/pages/Desktop/AppSettings/__test__/GeneralSettings.test.tsx @@ -3,17 +3,17 @@ import { MemoryRouter } from 'react-router-dom' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { getAlertIsPermanentlyIgnored } from '../../../redux/alerts' -import * as Shell from '../../../redux/shell' +import { i18n } from '../../../../i18n' +import { getAlertIsPermanentlyIgnored } from '../../../../redux/alerts' +import * as Shell from '../../../../redux/shell' import { GeneralSettings } from '../GeneralSettings' -vi.mock('../../../redux/config') -vi.mock('../../../redux/shell') -vi.mock('../../../redux/analytics') -vi.mock('../../../redux/alerts') +vi.mock('../../../../redux/config') +vi.mock('../../../../redux/shell') +vi.mock('../../../../redux/analytics') +vi.mock('../../../../redux/alerts') const render = (): ReturnType => { return renderWithProviders( diff --git a/app/src/pages/AppSettings/__test__/PrivacySettings.test.tsx b/app/src/pages/Desktop/AppSettings/__test__/PrivacySettings.test.tsx similarity index 81% rename from app/src/pages/AppSettings/__test__/PrivacySettings.test.tsx rename to app/src/pages/Desktop/AppSettings/__test__/PrivacySettings.test.tsx index 0b2f5a47f4c..94a37b4b6fe 100644 --- a/app/src/pages/AppSettings/__test__/PrivacySettings.test.tsx +++ b/app/src/pages/Desktop/AppSettings/__test__/PrivacySettings.test.tsx @@ -2,13 +2,13 @@ import * as React from 'react' import { vi, it, describe } from 'vitest' import { MemoryRouter } from 'react-router-dom' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { PrivacySettings } from '../PrivacySettings' -vi.mock('../../../redux/analytics') -vi.mock('../../../redux/config') +vi.mock('../../../../redux/analytics') +vi.mock('../../../../redux/config') const render = (): ReturnType => { return renderWithProviders( diff --git a/app/src/pages/AppSettings/index.tsx b/app/src/pages/Desktop/AppSettings/index.tsx similarity index 88% rename from app/src/pages/AppSettings/index.tsx rename to app/src/pages/Desktop/AppSettings/index.tsx index 61b5f619c96..831b9ee25b1 100644 --- a/app/src/pages/AppSettings/index.tsx +++ b/app/src/pages/Desktop/AppSettings/index.tsx @@ -15,15 +15,15 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import * as Config from '../../redux/config' +import * as Config from '../../../redux/config' import { GeneralSettings } from './GeneralSettings' import { PrivacySettings } from './PrivacySettings' import { AdvancedSettings } from './AdvancedSettings' -import { FeatureFlags } from '../../organisms/AppSettings/FeatureFlags' -import { NavTab } from '../../molecules/NavTab' -import { Line } from '../../atoms/structure' +import { FeatureFlags } from '../../../organisms/AppSettings/FeatureFlags' +import { NavTab } from '../../../molecules/NavTab' +import { Line } from '../../../atoms/structure' -import type { DesktopRouteParams, AppSettingsTab } from '../../App/types' +import type { DesktopRouteParams, AppSettingsTab } from '../../../App/types' export function AppSettings(): JSX.Element { const { t } = useTranslation('app_settings') diff --git a/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx b/app/src/pages/Desktop/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx similarity index 78% rename from app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx rename to app/src/pages/Desktop/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx index 7039e9836b5..b3249904391 100644 --- a/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx +++ b/app/src/pages/Desktop/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx @@ -3,26 +3,26 @@ import { vi, describe, it, beforeEach } from 'vitest' import { screen } from '@testing-library/react' import { MemoryRouter, Route, Routes } from 'react-router-dom' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' import { CalibrationDashboard } from '..' import { useCalibrationTaskList, useAttachedPipettes, -} from '../../../../organisms/Devices/hooks' +} from '../../../../../organisms/Devices/hooks' import { useDashboardCalibratePipOffset } from '../hooks/useDashboardCalibratePipOffset' import { useDashboardCalibrateTipLength } from '../hooks/useDashboardCalibrateTipLength' import { useDashboardCalibrateDeck } from '../hooks/useDashboardCalibrateDeck' -import { expectedTaskList } from '../../../../organisms/Devices/hooks/__fixtures__/taskListFixtures' -import { mockLeftProtoPipette } from '../../../../redux/pipettes/__fixtures__' -import { useNotifyAllRunsQuery } from '../../../../resources/runs' +import { expectedTaskList } from '../../../../../organisms/Devices/hooks/__fixtures__/taskListFixtures' +import { mockLeftProtoPipette } from '../../../../../redux/pipettes/__fixtures__' +import { useNotifyAllRunsQuery } from '../../../../../resources/runs' -vi.mock('../../../../organisms/Devices/hooks') +vi.mock('../../../../../organisms/Devices/hooks') vi.mock('../hooks/useDashboardCalibratePipOffset') vi.mock('../hooks/useDashboardCalibrateTipLength') vi.mock('../hooks/useDashboardCalibrateDeck') -vi.mock('../../../../resources/runs') +vi.mock('../../../../../resources/runs') const render = (path = '/') => { return renderWithProviders( diff --git a/app/src/pages/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibrateDeck.test.tsx b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibrateDeck.test.tsx similarity index 100% rename from app/src/pages/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibrateDeck.test.tsx rename to app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibrateDeck.test.tsx diff --git a/app/src/pages/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibratePipOffset.test.tsx b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibratePipOffset.test.tsx similarity index 100% rename from app/src/pages/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibratePipOffset.test.tsx rename to app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibratePipOffset.test.tsx diff --git a/app/src/pages/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibrateTipLength.test.tsx b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibrateTipLength.test.tsx similarity index 100% rename from app/src/pages/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibrateTipLength.test.tsx rename to app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/__tests__/useDashboardCalibrateTipLength.test.tsx diff --git a/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx similarity index 83% rename from app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx rename to app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx index 5a99adc3df8..5542768bf15 100644 --- a/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx +++ b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateDeck.tsx @@ -4,18 +4,18 @@ import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ModalShell } from '@opentrons/components' -import { getTopPortalEl } from '../../../../App/portal' -import { WizardHeader } from '../../../../molecules/WizardHeader' -import { CalibrateDeck } from '../../../../organisms/CalibrateDeck' -import { LoadingState } from '../../../../organisms/CalibrationPanels' -import * as RobotApi from '../../../../redux/robot-api' -import * as Sessions from '../../../../redux/sessions' -import { getDeckCalibrationSession } from '../../../../redux/sessions/deck-calibration/selectors' +import { getTopPortalEl } from '../../../../../App/portal' +import { WizardHeader } from '../../../../../molecules/WizardHeader' +import { CalibrateDeck } from '../../../../../organisms/CalibrateDeck' +import { LoadingState } from '../../../../../organisms/CalibrationPanels' +import * as RobotApi from '../../../../../redux/robot-api' +import * as Sessions from '../../../../../redux/sessions' +import { getDeckCalibrationSession } from '../../../../../redux/sessions/deck-calibration/selectors' -import type { State } from '../../../../redux/types' -import type { DeckCalibrationSession } from '../../../../redux/sessions' -import type { SessionCommandString } from '../../../../redux/sessions/types' -import type { RequestState } from '../../../../redux/robot-api/types' +import type { State } from '../../../../../redux/types' +import type { DeckCalibrationSession } from '../../../../../redux/sessions' +import type { SessionCommandString } from '../../../../../redux/sessions/types' +import type { RequestState } from '../../../../../redux/robot-api/types' // deck calibration commands for which the full page spinner should not appear const spinnerCommandBlockList: SessionCommandString[] = [ diff --git a/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset.tsx b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset.tsx similarity index 88% rename from app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset.tsx rename to app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset.tsx index 6ec2595899f..d48e690c824 100644 --- a/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset.tsx +++ b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibratePipOffset.tsx @@ -4,22 +4,22 @@ import { useSelector, useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' import { ModalShell } from '@opentrons/components' -import { getTopPortalEl } from '../../../../App/portal' -import { WizardHeader } from '../../../../molecules/WizardHeader' -import { CalibratePipetteOffset } from '../../../../organisms/CalibratePipetteOffset' -import { LoadingState } from '../../../../organisms/CalibrationPanels' -import * as RobotApi from '../../../../redux/robot-api' -import * as Sessions from '../../../../redux/sessions' -import { getPipetteOffsetCalibrationSession } from '../../../../redux/sessions/pipette-offset-calibration/selectors' -import { pipetteOffsetCalibrationStarted } from '../../../../redux/analytics' - -import type { State } from '../../../../redux/types' +import { getTopPortalEl } from '../../../../../App/portal' +import { WizardHeader } from '../../../../../molecules/WizardHeader' +import { CalibratePipetteOffset } from '../../../../../organisms/CalibratePipetteOffset' +import { LoadingState } from '../../../../../organisms/CalibrationPanels' +import * as RobotApi from '../../../../../redux/robot-api' +import * as Sessions from '../../../../../redux/sessions' +import { getPipetteOffsetCalibrationSession } from '../../../../../redux/sessions/pipette-offset-calibration/selectors' +import { pipetteOffsetCalibrationStarted } from '../../../../../redux/analytics' + +import type { State } from '../../../../../redux/types' import type { SessionCommandString, PipetteOffsetCalibrationSession, PipetteOffsetCalibrationSessionParams, -} from '../../../../redux/sessions/types' -import type { RequestState } from '../../../../redux/robot-api/types' +} from '../../../../../redux/sessions/types' +import type { RequestState } from '../../../../../redux/robot-api/types' // pipette calibration commands for which the full page spinner should not appear const spinnerCommandBlockList: SessionCommandString[] = [ diff --git a/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength.tsx b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength.tsx similarity index 86% rename from app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength.tsx rename to app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength.tsx index 8827c617564..04cc353db7e 100644 --- a/app/src/pages/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength.tsx +++ b/app/src/pages/Desktop/Devices/CalibrationDashboard/hooks/useDashboardCalibrateTipLength.tsx @@ -4,24 +4,24 @@ import { useSelector, useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' import { ModalShell } from '@opentrons/components' -import { getTopPortalEl } from '../../../../App/portal' -import { WizardHeader } from '../../../../molecules/WizardHeader' -import { CalibrateTipLength } from '../../../../organisms/CalibrateTipLength' -import { AskForCalibrationBlockModal } from '../../../../organisms/CalibrateTipLength/AskForCalibrationBlockModal' -import { LoadingState } from '../../../../organisms/CalibrationPanels' -import * as RobotApi from '../../../../redux/robot-api' -import * as Sessions from '../../../../redux/sessions' -import { tipLengthCalibrationStarted } from '../../../../redux/analytics' -import { getHasCalibrationBlock } from '../../../../redux/config' -import { getTipLengthCalibrationSession } from '../../../../redux/sessions/tip-length-calibration/selectors' - -import type { RequestState } from '../../../../redux/robot-api/types' +import { getTopPortalEl } from '../../../../../App/portal' +import { WizardHeader } from '../../../../../molecules/WizardHeader' +import { CalibrateTipLength } from '../../../../../organisms/CalibrateTipLength' +import { AskForCalibrationBlockModal } from '../../../../../organisms/CalibrateTipLength/AskForCalibrationBlockModal' +import { LoadingState } from '../../../../../organisms/CalibrationPanels' +import * as RobotApi from '../../../../../redux/robot-api' +import * as Sessions from '../../../../../redux/sessions' +import { tipLengthCalibrationStarted } from '../../../../../redux/analytics' +import { getHasCalibrationBlock } from '../../../../../redux/config' +import { getTipLengthCalibrationSession } from '../../../../../redux/sessions/tip-length-calibration/selectors' + +import type { RequestState } from '../../../../../redux/robot-api/types' import type { SessionCommandString, TipLengthCalibrationSession, TipLengthCalibrationSessionParams, -} from '../../../../redux/sessions/types' -import type { State } from '../../../../redux/types' +} from '../../../../../redux/sessions/types' +import type { State } from '../../../../../redux/types' // tip length calibration commands for which the full page spinner should not appear const spinnerCommandBlockList: SessionCommandString[] = [ diff --git a/app/src/pages/Devices/CalibrationDashboard/index.tsx b/app/src/pages/Desktop/Devices/CalibrationDashboard/index.tsx similarity index 82% rename from app/src/pages/Devices/CalibrationDashboard/index.tsx rename to app/src/pages/Desktop/Devices/CalibrationDashboard/index.tsx index b875918c25e..e3dff770f1e 100644 --- a/app/src/pages/Devices/CalibrationDashboard/index.tsx +++ b/app/src/pages/Desktop/Devices/CalibrationDashboard/index.tsx @@ -1,15 +1,15 @@ import * as React from 'react' import { useParams } from 'react-router-dom' import { ApiHostProvider } from '@opentrons/react-api-client' -import { CalibrationTaskList } from '../../../organisms/CalibrationTaskList' -import { OPENTRONS_USB } from '../../../redux/discovery' -import { appShellRequestor } from '../../../redux/shell/remote' +import { CalibrationTaskList } from '../../../../organisms/CalibrationTaskList' +import { OPENTRONS_USB } from '../../../../redux/discovery' +import { appShellRequestor } from '../../../../redux/shell/remote' import { useDashboardCalibrateDeck } from './hooks/useDashboardCalibrateDeck' import { useDashboardCalibratePipOffset } from './hooks/useDashboardCalibratePipOffset' import { useDashboardCalibrateTipLength } from './hooks/useDashboardCalibrateTipLength' -import { useRobot } from '../../../organisms/Devices/hooks' +import { useRobot } from '../../../../organisms/Devices/hooks' -import type { DesktopRouteParams } from '../../../App/types' +import type { DesktopRouteParams } from '../../../../App/types' export function CalibrationDashboard(): JSX.Element { const { robotName } = useParams< diff --git a/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx b/app/src/pages/Desktop/Devices/DeviceDetails/DeviceDetailsComponent.tsx similarity index 73% rename from app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx rename to app/src/pages/Desktop/Devices/DeviceDetails/DeviceDetailsComponent.tsx index 513bccb9874..3dccb792af0 100644 --- a/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx +++ b/app/src/pages/Desktop/Devices/DeviceDetails/DeviceDetailsComponent.tsx @@ -10,13 +10,16 @@ import { SPACING, } from '@opentrons/components' -import { DeviceDetailsDeckConfiguration } from '../../../organisms/DeviceDetailsDeckConfiguration' -import { RobotOverview } from '../../../organisms/Devices/RobotOverview' -import { InstrumentsAndModules } from '../../../organisms/Devices/InstrumentsAndModules' -import { RecentProtocolRuns } from '../../../organisms/Devices/RecentProtocolRuns' -import { EstopBanner } from '../../../organisms/Devices/EstopBanner' -import { DISENGAGED, useEstopContext } from '../../../organisms/EmergencyStop' -import { useIsFlex } from '../../../organisms/Devices/hooks' +import { DeviceDetailsDeckConfiguration } from '../../../../organisms/DeviceDetailsDeckConfiguration' +import { RobotOverview } from '../../../../organisms/Devices/RobotOverview' +import { InstrumentsAndModules } from '../../../../organisms/Devices/InstrumentsAndModules' +import { RecentProtocolRuns } from '../../../../organisms/Devices/RecentProtocolRuns' +import { EstopBanner } from '../../../../organisms/Devices/EstopBanner' +import { + DISENGAGED, + useEstopContext, +} from '../../../../organisms/EmergencyStop' +import { useIsFlex } from '../../../../organisms/Devices/hooks' interface DeviceDetailsComponentProps { robotName: string diff --git a/app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx b/app/src/pages/Desktop/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx similarity index 70% rename from app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx rename to app/src/pages/Desktop/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx index 7c45cf32baa..e44f32c103c 100644 --- a/app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx +++ b/app/src/pages/Desktop/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx @@ -4,27 +4,27 @@ import { when } from 'vitest-when' import { screen } from '@testing-library/react' import { MemoryRouter, Route, Routes } from 'react-router-dom' -import { renderWithProviders } from '../../../../__testing-utils__' +import { renderWithProviders } from '../../../../../__testing-utils__' -import { i18n } from '../../../../i18n' +import { i18n } from '../../../../../i18n' import { useRobot, useSyncRobotClock, -} from '../../../../organisms/Devices/hooks' -import { InstrumentsAndModules } from '../../../../organisms/Devices/InstrumentsAndModules' -import { RecentProtocolRuns } from '../../../../organisms/Devices/RecentProtocolRuns' -import { RobotOverview } from '../../../../organisms/Devices/RobotOverview' -import { getScanning } from '../../../../redux/discovery' -import { mockConnectableRobot } from '../../../../redux/discovery/__fixtures__' +} from '../../../../../organisms/Devices/hooks' +import { InstrumentsAndModules } from '../../../../../organisms/Devices/InstrumentsAndModules' +import { RecentProtocolRuns } from '../../../../../organisms/Devices/RecentProtocolRuns' +import { RobotOverview } from '../../../../../organisms/Devices/RobotOverview' +import { getScanning } from '../../../../../redux/discovery' +import { mockConnectableRobot } from '../../../../../redux/discovery/__fixtures__' import { DeviceDetails } from '..' -import type { State } from '../../../../redux/types' +import type { State } from '../../../../../redux/types' -vi.mock('../../../../organisms/Devices/hooks') -vi.mock('../../../../organisms/Devices/InstrumentsAndModules') -vi.mock('../../../../organisms/Devices/RecentProtocolRuns') -vi.mock('../../../../organisms/Devices/RobotOverview') -vi.mock('../../../../redux/discovery') +vi.mock('../../../../../organisms/Devices/hooks') +vi.mock('../../../../../organisms/Devices/InstrumentsAndModules') +vi.mock('../../../../../organisms/Devices/RecentProtocolRuns') +vi.mock('../../../../../organisms/Devices/RobotOverview') +vi.mock('../../../../../redux/discovery') const render = (path = '/') => { return renderWithProviders( diff --git a/app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetailsComponent.test.tsx b/app/src/pages/Desktop/Devices/DeviceDetails/__tests__/DeviceDetailsComponent.test.tsx similarity index 69% rename from app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetailsComponent.test.tsx rename to app/src/pages/Desktop/Devices/DeviceDetails/__tests__/DeviceDetailsComponent.test.tsx index 00e0c1eff9f..4905abd3393 100644 --- a/app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetailsComponent.test.tsx +++ b/app/src/pages/Desktop/Devices/DeviceDetails/__tests__/DeviceDetailsComponent.test.tsx @@ -2,25 +2,25 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { when } from 'vitest-when' -import { renderWithProviders } from '../../../../__testing-utils__' +import { renderWithProviders } from '../../../../../__testing-utils__' import { useEstopQuery } from '@opentrons/react-api-client' -import { i18n } from '../../../../i18n' -import { InstrumentsAndModules } from '../../../../organisms/Devices/InstrumentsAndModules' -import { RecentProtocolRuns } from '../../../../organisms/Devices/RecentProtocolRuns' -import { RobotOverview } from '../../../../organisms/Devices/RobotOverview' -import { DISENGAGED, NOT_PRESENT } from '../../../../organisms/EmergencyStop' -import { DeviceDetailsDeckConfiguration } from '../../../../organisms/DeviceDetailsDeckConfiguration' -import { useIsFlex } from '../../../../organisms/Devices/hooks' +import { i18n } from '../../../../../i18n' +import { InstrumentsAndModules } from '../../../../../organisms/Devices/InstrumentsAndModules' +import { RecentProtocolRuns } from '../../../../../organisms/Devices/RecentProtocolRuns' +import { RobotOverview } from '../../../../../organisms/Devices/RobotOverview' +import { DISENGAGED, NOT_PRESENT } from '../../../../../organisms/EmergencyStop' +import { DeviceDetailsDeckConfiguration } from '../../../../../organisms/DeviceDetailsDeckConfiguration' +import { useIsFlex } from '../../../../../organisms/Devices/hooks' import { DeviceDetailsComponent } from '../DeviceDetailsComponent' vi.mock('@opentrons/react-api-client') -vi.mock('../../../../organisms/Devices/hooks') -vi.mock('../../../../organisms/Devices/InstrumentsAndModules') -vi.mock('../../../../organisms/Devices/RecentProtocolRuns') -vi.mock('../../../../organisms/Devices/RobotOverview') -vi.mock('../../../../organisms/DeviceDetailsDeckConfiguration') -vi.mock('../../../../redux/discovery') +vi.mock('../../../../../organisms/Devices/hooks') +vi.mock('../../../../../organisms/Devices/InstrumentsAndModules') +vi.mock('../../../../../organisms/Devices/RecentProtocolRuns') +vi.mock('../../../../../organisms/Devices/RobotOverview') +vi.mock('../../../../../organisms/DeviceDetailsDeckConfiguration') +vi.mock('../../../../../redux/discovery') const ROBOT_NAME = 'otie' const mockEstopStatus = { diff --git a/app/src/pages/Devices/DeviceDetails/index.tsx b/app/src/pages/Desktop/Devices/DeviceDetails/index.tsx similarity index 76% rename from app/src/pages/Devices/DeviceDetails/index.tsx rename to app/src/pages/Desktop/Devices/DeviceDetails/index.tsx index 962dd229d87..543a0f58be8 100644 --- a/app/src/pages/Devices/DeviceDetails/index.tsx +++ b/app/src/pages/Desktop/Devices/DeviceDetails/index.tsx @@ -4,12 +4,15 @@ import { Navigate, useParams } from 'react-router-dom' import { ApiHostProvider } from '@opentrons/react-api-client' -import { useRobot, useSyncRobotClock } from '../../../organisms/Devices/hooks' -import { getScanning, OPENTRONS_USB } from '../../../redux/discovery' -import { appShellRequestor } from '../../../redux/shell/remote' +import { + useRobot, + useSyncRobotClock, +} from '../../../../organisms/Devices/hooks' +import { getScanning, OPENTRONS_USB } from '../../../../redux/discovery' +import { appShellRequestor } from '../../../../redux/shell/remote' import { DeviceDetailsComponent } from './DeviceDetailsComponent' -import type { DesktopRouteParams } from '../../../App/types' +import type { DesktopRouteParams } from '../../../../App/types' export function DeviceDetails(): JSX.Element | null { const { robotName } = useParams< diff --git a/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx b/app/src/pages/Desktop/Devices/DevicesLanding/NewRobotSetupHelp.tsx similarity index 87% rename from app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx rename to app/src/pages/Desktop/Devices/DevicesLanding/NewRobotSetupHelp.tsx index 729b4448af5..a0b54194621 100644 --- a/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx +++ b/app/src/pages/Desktop/Devices/DevicesLanding/NewRobotSetupHelp.tsx @@ -14,8 +14,8 @@ import { Modal, } from '@opentrons/components' -import { getTopPortalEl } from '../../../App/portal' -import { ExternalLink } from '../../../atoms/Link/ExternalLink' +import { getTopPortalEl } from '../../../../App/portal' +import { ExternalLink } from '../../../../atoms/Link/ExternalLink' const NEW_FLEX_SETUP_SUPPORT_ARTICLE_HREF = 'https://insights.opentrons.com/hubfs/Products/Flex/Opentrons%20Flex%20Quickstart%20Guide.pdf' @@ -23,7 +23,7 @@ const NEW_OT2_SETUP_SUPPORT_ARTICLE_HREF = 'https://insights.opentrons.com/hubfs/Products/OT-2/OT-2%20Quick%20Start%20Guide.pdf' export function NewRobotSetupHelp(): JSX.Element { - const { t } = useTranslation(['devices_landing', 'shared']) + const { t } = useTranslation(['devices_landing', 'shared', 'branded']) const [showNewRobotHelpModal, setShowNewRobotHelpModal] = React.useState( false ) @@ -49,13 +49,13 @@ export function NewRobotSetupHelp(): JSX.Element { > - {t('new_robot_instructions')} + {t('branded:new_robot_instructions')} - {t('opentrons_flex_quickstart_guide')} + {t('branded:opentrons_flex_quickstart_guide')} { return renderWithProviders(, { diff --git a/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx b/app/src/pages/Desktop/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx similarity index 95% rename from app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx rename to app/src/pages/Desktop/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx index fb4cbadfd42..ef9a3bbcf50 100644 --- a/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx +++ b/app/src/pages/Desktop/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { it, describe, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' import { NewRobotSetupHelp } from '../NewRobotSetupHelp' const render = () => { diff --git a/app/src/pages/Devices/DevicesLanding/index.tsx b/app/src/pages/Desktop/Devices/DevicesLanding/index.tsx similarity index 92% rename from app/src/pages/Devices/DevicesLanding/index.tsx rename to app/src/pages/Desktop/Devices/DevicesLanding/index.tsx index 1b17556882a..1423d7e8f32 100644 --- a/app/src/pages/Devices/DevicesLanding/index.tsx +++ b/app/src/pages/Desktop/Devices/DevicesLanding/index.tsx @@ -26,16 +26,16 @@ import { getReachableRobots, getUnreachableRobots, OPENTRONS_USB, -} from '../../../redux/discovery' -import { appShellRequestor } from '../../../redux/shell/remote' -import { RobotCard } from '../../../organisms/Devices/RobotCard' -import { DevicesEmptyState } from '../../../organisms/Devices/DevicesEmptyState' -import { CollapsibleSection } from '../../../molecules/CollapsibleSection' +} from '../../../../redux/discovery' +import { appShellRequestor } from '../../../../redux/shell/remote' +import { RobotCard } from '../../../../organisms/Devices/RobotCard' +import { DevicesEmptyState } from '../../../../organisms/Devices/DevicesEmptyState' +import { CollapsibleSection } from '../../../../molecules/CollapsibleSection' -import { Divider } from '../../../atoms/structure' +import { Divider } from '../../../../atoms/structure' import { NewRobotSetupHelp } from './NewRobotSetupHelp' -import type { State } from '../../../redux/types' +import type { State } from '../../../../redux/types' export const TROUBLESHOOTING_CONNECTION_PROBLEMS_URL = 'https://support.opentrons.com/en/articles/2687601-troubleshooting-connection-problems' diff --git a/app/src/pages/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx b/app/src/pages/Desktop/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx similarity index 79% rename from app/src/pages/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx rename to app/src/pages/Desktop/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx index c5543f06d8c..3d3317ef021 100644 --- a/app/src/pages/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx +++ b/app/src/pages/Desktop/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx @@ -2,42 +2,45 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { Route, MemoryRouter, Routes } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' +import { when } from 'vitest-when' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../i18n' -import { mockConnectableRobot } from '../../../../redux/discovery/__fixtures__' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../i18n' +import { mockConnectableRobot } from '../../../../../redux/discovery/__fixtures__' import { useModuleRenderInfoForProtocolById, useRobot, useRunStatuses, useSyncRobotClock, -} from '../../../../organisms/Devices/hooks' -import { useMostRecentCompletedAnalysis } from '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { ProtocolRunHeader } from '../../../../organisms/Devices/ProtocolRun/ProtocolRunHeader' -import { ProtocolRunModuleControls } from '../../../../organisms/Devices/ProtocolRun/ProtocolRunModuleControls' -import { ProtocolRunSetup } from '../../../../organisms/Devices/ProtocolRun/ProtocolRunSetup' -import { RunPreviewComponent } from '../../../../organisms/RunPreview' -import { ProtocolRunRuntimeParameters } from '../../../../organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters' -import { useCurrentRunId } from '../../../../resources/runs' -import { mockRobotSideAnalysis } from '../../../../molecules/Command/__fixtures__' -import { useFeatureFlag } from '../../../../redux/config' + useRunHasStarted, +} from '../../../../../organisms/Devices/hooks' +import { useMostRecentCompletedAnalysis } from '../../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { ProtocolRunHeader } from '../../../../../organisms/Devices/ProtocolRun/ProtocolRunHeader' +import { ProtocolRunModuleControls } from '../../../../../organisms/Devices/ProtocolRun/ProtocolRunModuleControls' +import { ProtocolRunSetup } from '../../../../../organisms/Devices/ProtocolRun/ProtocolRunSetup' +import { RunPreviewComponent } from '../../../../../organisms/RunPreview' +import { ProtocolRunRuntimeParameters } from '../../../../../organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters' +import { useCurrentRunId } from '../../../../../resources/runs' +import { mockRobotSideAnalysis } from '../../../../../molecules/Command/__fixtures__' import { ProtocolRunDetails } from '..' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' vi.mock( - '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' + '../../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) -vi.mock('../../../../organisms/Devices/hooks') -vi.mock('../../../../organisms/Devices/ProtocolRun/ProtocolRunHeader') -vi.mock('../../../../organisms/Devices/ProtocolRun/ProtocolRunSetup') -vi.mock('../../../../organisms/RunPreview') -vi.mock('../../../../organisms/Devices/ProtocolRun/ProtocolRunModuleControls') -vi.mock('../../../../resources/runs') +vi.mock('../../../../../organisms/Devices/hooks') +vi.mock('../../../../../organisms/Devices/ProtocolRun/ProtocolRunHeader') +vi.mock('../../../../../organisms/Devices/ProtocolRun/ProtocolRunSetup') +vi.mock('../../../../../organisms/RunPreview') vi.mock( - '../../../../organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters' + '../../../../../organisms/Devices/ProtocolRun/ProtocolRunModuleControls' ) -vi.mock('../../../../redux/config') +vi.mock('../../../../../resources/runs') +vi.mock( + '../../../../../organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters' +) +vi.mock('../../../../../redux/config') const MOCK_MAGNETIC_MODULE_COORDS = [10, 20, 0] @@ -78,7 +81,6 @@ const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b' describe('ProtocolRunDetails', () => { beforeEach(() => { - vi.mocked(useFeatureFlag).mockReturnValue(false) vi.mocked(useRobot).mockReturnValue(mockConnectableRobot) vi.mocked(useRunStatuses).mockReturnValue({ isRunRunning: false, @@ -116,6 +118,7 @@ describe('ProtocolRunDetails', () => { vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue( mockRobotSideAnalysis ) + when(vi.mocked(useRunHasStarted)).calledWith(RUN_ID).thenReturn(false) }) afterEach(() => { vi.resetAllMocks() @@ -219,8 +222,8 @@ describe('ProtocolRunDetails', () => { expect(screen.queryByText('Mock RunPreview')).toBeFalsy() }) - it('redirects to the run tab when the run is not current', () => { - vi.mocked(useCurrentRunId).mockReturnValue(null) + it('redirects to the run tab when the run is started by ODD or another Desktop app', () => { + when(vi.mocked(useRunHasStarted)).calledWith(RUN_ID).thenReturn(true) render(`/devices/otie/protocol-runs/${RUN_ID}/setup`) screen.getByText('Mock RunPreview') @@ -228,7 +231,6 @@ describe('ProtocolRunDetails', () => { }) it('renders Parameters tab when runtime parameters ff is on', () => { - vi.mocked(useFeatureFlag).mockReturnValue(true) render(`/devices/otie/protocol-runs/${RUN_ID}/setup`) screen.getByText('Setup') @@ -238,7 +240,6 @@ describe('ProtocolRunDetails', () => { }) it('renders protocol run parameters when the parameters tab is clicked', () => { - vi.mocked(useFeatureFlag).mockReturnValue(true) render(`/devices/otie/protocol-runs/${RUN_ID}`) const parametersTab = screen.getByText('Parameters') diff --git a/app/src/pages/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx similarity index 87% rename from app/src/pages/Devices/ProtocolRunDetails/index.tsx rename to app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx index dd8e726cdcc..eb17aa5ba3f 100644 --- a/app/src/pages/Devices/ProtocolRunDetails/index.tsx +++ b/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx @@ -26,30 +26,31 @@ import { useModuleRenderInfoForProtocolById, useRobot, useRobotType, + useRunHasStarted, useRunStatuses, useSyncRobotClock, -} from '../../../organisms/Devices/hooks' -import { ProtocolRunHeader } from '../../../organisms/Devices/ProtocolRun/ProtocolRunHeader' -import { RunPreview } from '../../../organisms/RunPreview' +} from '../../../../organisms/Devices/hooks' +import { ProtocolRunHeader } from '../../../../organisms/Devices/ProtocolRun/ProtocolRunHeader' +import { RunPreview } from '../../../../organisms/RunPreview' import { ProtocolRunSetup, initialMissingSteps, -} from '../../../organisms/Devices/ProtocolRun/ProtocolRunSetup' -import { BackToTopButton } from '../../../organisms/Devices/ProtocolRun/BackToTopButton' -import { ProtocolRunModuleControls } from '../../../organisms/Devices/ProtocolRun/ProtocolRunModuleControls' -import { ProtocolRunRuntimeParameters } from '../../../organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters' -import { useCurrentRunId } from '../../../resources/runs' -import { OPENTRONS_USB } from '../../../redux/discovery' -import { fetchProtocols } from '../../../redux/protocol-storage' -import { appShellRequestor } from '../../../redux/shell/remote' -import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +} from '../../../../organisms/Devices/ProtocolRun/ProtocolRunSetup' +import { BackToTopButton } from '../../../../organisms/Devices/ProtocolRun/BackToTopButton' +import { ProtocolRunModuleControls } from '../../../../organisms/Devices/ProtocolRun/ProtocolRunModuleControls' +import { ProtocolRunRuntimeParameters } from '../../../../organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters' +import { useCurrentRunId } from '../../../../resources/runs' +import { OPENTRONS_USB } from '../../../../redux/discovery' +import { fetchProtocols } from '../../../../redux/protocol-storage' +import { appShellRequestor } from '../../../../redux/shell/remote' +import { useMostRecentCompletedAnalysis } from '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import type { ViewportListRef } from 'react-viewport-list' import type { DesktopRouteParams, ProtocolRunDetailsTab, -} from '../../../App/types' -import type { Dispatch } from '../../../redux/types' +} from '../../../../App/types' +import type { Dispatch } from '../../../../redux/types' const baseRoundTabStyling = css` ${TYPOGRAPHY.pSemiBold} @@ -315,16 +316,19 @@ const SetupTab = (props: SetupTabProps): JSX.Element | null => { const { t } = useTranslation('run_details') const currentRunId = useCurrentRunId() const navigate = useNavigate() - + const runHasStarted = useRunHasStarted(currentRunId) const disabled = currentRunId !== runId const tabDisabledReason = `${t('setup')} ${t( 'not_available_for_a_completed_run' )}` + // On the initial render or when a run first begins, navigate to "run preview" if the run has started. + // If "run again" is clicked, the user should NOT be directed back to the "setup" tab. React.useEffect(() => { - if (disabled && protocolRunDetailsTab === 'setup') + if (runHasStarted && protocolRunDetailsTab !== 'run-preview') { navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) - }, [disabled, navigate, robotName, runId]) + } + }, [runHasStarted]) return ( { replace: true, }) } - }, [disabled, navigate, robotName, runId]) + }, [disabled, navigate, protocolRunDetailsTab, robotName, runId]) return ( { if (disabled && protocolRunDetailsTab === 'module-controls') navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) - }, [disabled, navigate, robotName, runId]) + }, [disabled, navigate, protocolRunDetailsTab, robotName, runId]) return isEmpty(moduleRenderInfoForProtocolById) ? null : ( { return renderWithProviders( diff --git a/app/src/pages/Devices/RobotSettings/index.tsx b/app/src/pages/Desktop/Devices/RobotSettings/index.tsx similarity index 82% rename from app/src/pages/Devices/RobotSettings/index.tsx rename to app/src/pages/Desktop/Devices/RobotSettings/index.tsx index fd2e089a7d4..57d23297e56 100644 --- a/app/src/pages/Devices/RobotSettings/index.tsx +++ b/app/src/pages/Desktop/Devices/RobotSettings/index.tsx @@ -21,21 +21,24 @@ import { UNREACHABLE, REACHABLE, OPENTRONS_USB, -} from '../../../redux/discovery' -import { appShellRequestor } from '../../../redux/shell/remote' -import { getRobotUpdateSession } from '../../../redux/robot-update' -import { getDevtoolsEnabled } from '../../../redux/config' -import { Banner } from '../../../atoms/Banner' -import { useRobot } from '../../../organisms/Devices/hooks' -import { Line } from '../../../atoms/structure' -import { NavTab } from '../../../molecules/NavTab' -import { RobotSettingsCalibration } from '../../../organisms/RobotSettingsCalibration' -import { RobotSettingsAdvanced } from '../../../organisms/Devices/RobotSettings/RobotSettingsAdvanced' -import { RobotSettingsNetworking } from '../../../organisms/Devices/RobotSettings/RobotSettingsNetworking' -import { RobotSettingsFeatureFlags } from '../../../organisms/Devices/RobotSettings/RobotSettingsFeatureFlags' -import { ReachableBanner } from '../../../organisms/Devices/ReachableBanner' +} from '../../../../redux/discovery' +import { appShellRequestor } from '../../../../redux/shell/remote' +import { getRobotUpdateSession } from '../../../../redux/robot-update' +import { getDevtoolsEnabled } from '../../../../redux/config' +import { Banner } from '../../../../atoms/Banner' +import { useRobot } from '../../../../organisms/Devices/hooks' +import { Line } from '../../../../atoms/structure' +import { NavTab } from '../../../../molecules/NavTab' +import { RobotSettingsCalibration } from '../../../../organisms/RobotSettingsCalibration' +import { RobotSettingsAdvanced } from '../../../../organisms/Devices/RobotSettings/RobotSettingsAdvanced' +import { RobotSettingsNetworking } from '../../../../organisms/Devices/RobotSettings/RobotSettingsNetworking' +import { RobotSettingsFeatureFlags } from '../../../../organisms/Devices/RobotSettings/RobotSettingsFeatureFlags' +import { ReachableBanner } from '../../../../organisms/Devices/ReachableBanner' -import type { DesktopRouteParams, RobotSettingsTab } from '../../../App/types' +import type { + DesktopRouteParams, + RobotSettingsTab, +} from '../../../../App/types' export function RobotSettings(): JSX.Element | null { const { t } = useTranslation('device_settings') diff --git a/app/src/pages/Labware/__tests__/Labware.test.tsx b/app/src/pages/Desktop/Labware/__tests__/Labware.test.tsx similarity index 87% rename from app/src/pages/Labware/__tests__/Labware.test.tsx rename to app/src/pages/Desktop/Labware/__tests__/Labware.test.tsx index 62c3f6a4521..649a4bbac5d 100644 --- a/app/src/pages/Labware/__tests__/Labware.test.tsx +++ b/app/src/pages/Desktop/Labware/__tests__/Labware.test.tsx @@ -2,24 +2,24 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { useTrackEvent, ANALYTICS_OPEN_LABWARE_CREATOR_FROM_BOTTOM_OF_LABWARE_LIBRARY_LIST, -} from '../../../redux/analytics' -import { LabwareCard } from '../../../organisms/LabwareCard' -import { AddCustomLabwareSlideout } from '../../../organisms/AddCustomLabwareSlideout' -import { useToaster } from '../../../organisms/ToasterOven' +} from '../../../../redux/analytics' +import { LabwareCard } from '../../../../organisms/LabwareCard' +import { AddCustomLabwareSlideout } from '../../../../organisms/AddCustomLabwareSlideout' +import { useToaster } from '../../../../organisms/ToasterOven' import { useAllLabware, useLabwareFailure, useNewLabwareName } from '../hooks' import { Labware } from '..' -import { mockDefinition } from '../../../redux/custom-labware/__fixtures__' +import { mockDefinition } from '../../../../redux/custom-labware/__fixtures__' -vi.mock('../../../organisms/LabwareCard') -vi.mock('../../../organisms/AddCustomLabwareSlideout') -vi.mock('../../../organisms/ToasterOven') +vi.mock('../../../../organisms/LabwareCard') +vi.mock('../../../../organisms/AddCustomLabwareSlideout') +vi.mock('../../../../organisms/ToasterOven') vi.mock('../hooks') -vi.mock('../../../redux/analytics') +vi.mock('../../../../redux/analytics') const mockTrackEvent = vi.fn() const mockMakeSnackbar = vi.fn() diff --git a/app/src/pages/Labware/__tests__/hooks.test.tsx b/app/src/pages/Desktop/Labware/__tests__/hooks.test.tsx similarity index 95% rename from app/src/pages/Labware/__tests__/hooks.test.tsx rename to app/src/pages/Desktop/Labware/__tests__/hooks.test.tsx index 23035d94cc3..3824500f8a4 100644 --- a/app/src/pages/Labware/__tests__/hooks.test.tsx +++ b/app/src/pages/Desktop/Labware/__tests__/hooks.test.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux' import { createStore } from 'redux' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { renderHook } from '@testing-library/react' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { I18nextProvider } from 'react-i18next' import { getAllDefs } from '../helpers/getAllDefs' @@ -11,19 +11,19 @@ import { getValidCustomLabware, getAddLabwareFailure, getAddNewLabwareName, -} from '../../../redux/custom-labware' +} from '../../../../redux/custom-labware' import { mockDefinition, mockValidLabware, -} from '../../../redux/custom-labware/__fixtures__' +} from '../../../../redux/custom-labware/__fixtures__' import { useAllLabware, useLabwareFailure, useNewLabwareName } from '../hooks' import type { Store } from 'redux' -import type { State } from '../../../redux/types' -import type { FailedLabwareFile } from '../../../redux/custom-labware/types' +import type { State } from '../../../../redux/types' +import type { FailedLabwareFile } from '../../../../redux/custom-labware/types' -vi.mock('../../../redux/custom-labware') +vi.mock('../../../../redux/custom-labware') vi.mock('../helpers/getAllDefs') describe('useAllLabware hook', () => { diff --git a/app/src/pages/Labware/helpers/__mocks__/getAllDefs.ts b/app/src/pages/Desktop/Labware/helpers/__mocks__/getAllDefs.ts similarity index 100% rename from app/src/pages/Labware/helpers/__mocks__/getAllDefs.ts rename to app/src/pages/Desktop/Labware/helpers/__mocks__/getAllDefs.ts diff --git a/app/src/pages/Labware/helpers/definitions.ts b/app/src/pages/Desktop/Labware/helpers/definitions.ts similarity index 100% rename from app/src/pages/Labware/helpers/definitions.ts rename to app/src/pages/Desktop/Labware/helpers/definitions.ts diff --git a/app/src/pages/Labware/helpers/getAllDefs.ts b/app/src/pages/Desktop/Labware/helpers/getAllDefs.ts similarity index 100% rename from app/src/pages/Labware/helpers/getAllDefs.ts rename to app/src/pages/Desktop/Labware/helpers/getAllDefs.ts diff --git a/app/src/pages/Labware/hooks.tsx b/app/src/pages/Desktop/Labware/hooks.tsx similarity index 97% rename from app/src/pages/Labware/hooks.tsx rename to app/src/pages/Desktop/Labware/hooks.tsx index b1453738652..c8e973682cd 100644 --- a/app/src/pages/Labware/hooks.tsx +++ b/app/src/pages/Desktop/Labware/hooks.tsx @@ -6,9 +6,9 @@ import { getAddNewLabwareName, clearNewLabwareName, getValidCustomLabware, -} from '../../redux/custom-labware' +} from '../../../redux/custom-labware' import { getAllDefinitions } from './helpers/definitions' -import type { Dispatch } from '../../redux/types' +import type { Dispatch } from '../../../redux/types' import type { LabwareDefinition2 as LabwareDefinition } from '@opentrons/shared-data' import type { LabwareFilter, LabwareSort } from './types' diff --git a/app/src/pages/Labware/index.tsx b/app/src/pages/Desktop/Labware/index.tsx similarity index 95% rename from app/src/pages/Labware/index.tsx rename to app/src/pages/Desktop/Labware/index.tsx index dc464c306fb..7239e802771 100644 --- a/app/src/pages/Labware/index.tsx +++ b/app/src/pages/Desktop/Labware/index.tsx @@ -32,13 +32,13 @@ import { LabwareCreator } from '@opentrons/labware-library' import { useTrackEvent, ANALYTICS_OPEN_LABWARE_CREATOR_FROM_BOTTOM_OF_LABWARE_LIBRARY_LIST, -} from '../../redux/analytics' -import { addCustomLabwareFileFromCreator } from '../../redux/custom-labware' -import { LabwareCard } from '../../organisms/LabwareCard' -import { AddCustomLabwareSlideout } from '../../organisms/AddCustomLabwareSlideout' -import { LabwareDetails } from '../../organisms/LabwareDetails' -import { useToaster } from '../../organisms/ToasterOven' -import { useFeatureFlag } from '../../redux/config' +} from '../../../redux/analytics' +import { addCustomLabwareFileFromCreator } from '../../../redux/custom-labware' +import { LabwareCard } from '../../../organisms/LabwareCard' +import { AddCustomLabwareSlideout } from '../../../organisms/AddCustomLabwareSlideout' +import { LabwareDetails } from '../../../organisms/LabwareDetails' +import { useToaster } from '../../../organisms/ToasterOven' +import { useFeatureFlag } from '../../../redux/config' import { useAllLabware, useLabwareFailure, useNewLabwareName } from './hooks' import type { DropdownOption } from '@opentrons/components' diff --git a/app/src/pages/Labware/types.ts b/app/src/pages/Desktop/Labware/types.ts similarity index 100% rename from app/src/pages/Labware/types.ts rename to app/src/pages/Desktop/Labware/types.ts diff --git a/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx b/app/src/pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline.tsx similarity index 79% rename from app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx rename to app/src/pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline.tsx index b8511ad4f87..1f3b32796cf 100644 --- a/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline.tsx @@ -5,11 +5,11 @@ import { Icon, Box, SPACING } from '@opentrons/components' import { fetchProtocols, getStoredProtocol, -} from '../../../redux/protocol-storage' -import { ProtocolTimelineScrubber } from '../../../organisms/ProtocolTimelineScrubber' +} from '../../../../redux/protocol-storage' +import { ProtocolTimelineScrubber } from '../../../../organisms/ProtocolTimelineScrubber' -import type { Dispatch, State } from '../../../redux/types' -import type { DesktopRouteParams } from '../../../App/types' +import type { Dispatch, State } from '../../../../redux/types' +import type { DesktopRouteParams } from '../../../../App/types' export function ProtocolTimeline(): JSX.Element { const { protocolKey } = useParams< diff --git a/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/Desktop/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx similarity index 81% rename from app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx rename to app/src/pages/Desktop/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index 27e728dd844..a4e97c9f544 100644 --- a/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -3,21 +3,21 @@ import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' import { Route, MemoryRouter, Routes } from 'react-router-dom' import { when } from 'vitest-when' -import { renderWithProviders } from '../../../../__testing-utils__' +import { renderWithProviders } from '../../../../../__testing-utils__' -import { i18n } from '../../../../i18n' -import { getStoredProtocol } from '../../../../redux/protocol-storage' -import { storedProtocolData } from '../../../../redux/protocol-storage/__fixtures__' -import { ProtocolDetails as ProtocolDetailsContents } from '../../../../organisms/ProtocolDetails' +import { i18n } from '../../../../../i18n' +import { getStoredProtocol } from '../../../../../redux/protocol-storage' +import { storedProtocolData } from '../../../../../redux/protocol-storage/__fixtures__' +import { ProtocolDetails as ProtocolDetailsContents } from '../../../../../organisms/ProtocolDetails' import { ProtocolDetails } from '../' -import type { State } from '../../../../redux/types' +import type { State } from '../../../../../redux/types' const mockProtocolKey = 'protocolKeyStub' -vi.mock('../../../../redux/protocol-storage') -vi.mock('../../../../organisms/ProtocolDetails') +vi.mock('../../../../../redux/protocol-storage') +vi.mock('../../../../../organisms/ProtocolDetails') const MOCK_STATE: State = { protocolStorage: { diff --git a/app/src/pages/Protocols/ProtocolDetails/index.tsx b/app/src/pages/Desktop/Protocols/ProtocolDetails/index.tsx similarity index 78% rename from app/src/pages/Protocols/ProtocolDetails/index.tsx rename to app/src/pages/Desktop/Protocols/ProtocolDetails/index.tsx index a75e3457540..a152a5d0bfc 100644 --- a/app/src/pages/Protocols/ProtocolDetails/index.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolDetails/index.tsx @@ -5,11 +5,11 @@ import { useDispatch, useSelector } from 'react-redux' import { fetchProtocols, getStoredProtocol, -} from '../../../redux/protocol-storage' -import { ProtocolDetails as ProtocolDetailsContents } from '../../../organisms/ProtocolDetails' +} from '../../../../redux/protocol-storage' +import { ProtocolDetails as ProtocolDetailsContents } from '../../../../organisms/ProtocolDetails' -import type { Dispatch, State } from '../../../redux/types' -import type { DesktopRouteParams } from '../../../App/types' +import type { Dispatch, State } from '../../../../redux/types' +import type { DesktopRouteParams } from '../../../../App/types' export function ProtocolDetails(): JSX.Element { const { protocolKey } = useParams< diff --git a/app/src/pages/Protocols/ProtocolsLanding/__tests__/ProtocolsLanding.test.tsx b/app/src/pages/Desktop/Protocols/ProtocolsLanding/__tests__/ProtocolsLanding.test.tsx similarity index 56% rename from app/src/pages/Protocols/ProtocolsLanding/__tests__/ProtocolsLanding.test.tsx rename to app/src/pages/Desktop/Protocols/ProtocolsLanding/__tests__/ProtocolsLanding.test.tsx index e56fb6584c8..1235ba08e20 100644 --- a/app/src/pages/Protocols/ProtocolsLanding/__tests__/ProtocolsLanding.test.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolsLanding/__tests__/ProtocolsLanding.test.tsx @@ -1,17 +1,17 @@ import * as React from 'react' import { vi, it, describe } from 'vitest' import { screen } from '@testing-library/react' -import { renderWithProviders } from '../../../../__testing-utils__' +import { renderWithProviders } from '../../../../../__testing-utils__' -import { ProtocolsEmptyState } from '../../../../organisms/ProtocolsLanding/ProtocolsEmptyState' -import { getStoredProtocols } from '../../../../redux/protocol-storage' -import { storedProtocolData } from '../../../../redux/protocol-storage/__fixtures__' -import { ProtocolList } from '../../../../organisms/ProtocolsLanding/ProtocolList' +import { ProtocolsEmptyState } from '../../../../../organisms/ProtocolsLanding/ProtocolsEmptyState' +import { getStoredProtocols } from '../../../../../redux/protocol-storage' +import { storedProtocolData } from '../../../../../redux/protocol-storage/__fixtures__' +import { ProtocolList } from '../../../../../organisms/ProtocolsLanding/ProtocolList' import { ProtocolsLanding } from '..' -vi.mock('../../../../redux/protocol-storage') -vi.mock('../../../../organisms/ProtocolsLanding/ProtocolsEmptyState') -vi.mock('../../../../organisms/ProtocolsLanding/ProtocolList') +vi.mock('../../../../../redux/protocol-storage') +vi.mock('../../../../../organisms/ProtocolsLanding/ProtocolsEmptyState') +vi.mock('../../../../../organisms/ProtocolsLanding/ProtocolList') const render = () => { return renderWithProviders()[0] diff --git a/app/src/pages/Protocols/ProtocolsLanding/index.tsx b/app/src/pages/Desktop/Protocols/ProtocolsLanding/index.tsx similarity index 64% rename from app/src/pages/Protocols/ProtocolsLanding/index.tsx rename to app/src/pages/Desktop/Protocols/ProtocolsLanding/index.tsx index 9fb344fb46d..63a21c4f50b 100644 --- a/app/src/pages/Protocols/ProtocolsLanding/index.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolsLanding/index.tsx @@ -3,11 +3,11 @@ import { useDispatch, useSelector } from 'react-redux' import { fetchProtocols, getStoredProtocols, -} from '../../../redux/protocol-storage' -import { ProtocolsEmptyState } from '../../../organisms/ProtocolsLanding/ProtocolsEmptyState' -import { ProtocolList } from '../../../organisms/ProtocolsLanding/ProtocolList' +} from '../../../../redux/protocol-storage' +import { ProtocolsEmptyState } from '../../../../organisms/ProtocolsLanding/ProtocolsEmptyState' +import { ProtocolList } from '../../../../organisms/ProtocolsLanding/ProtocolList' -import type { Dispatch, State } from '../../../redux/types' +import type { Dispatch, State } from '../../../../redux/types' export function ProtocolsLanding(): JSX.Element { const dispatch = useDispatch() diff --git a/app/src/pages/Desktop/Protocols/hooks/__tests__/hooks.test.tsx b/app/src/pages/Desktop/Protocols/hooks/__tests__/hooks.test.tsx new file mode 100644 index 00000000000..b72b60bd77f --- /dev/null +++ b/app/src/pages/Desktop/Protocols/hooks/__tests__/hooks.test.tsx @@ -0,0 +1,249 @@ +import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' +import { renderHook } from '@testing-library/react' +import { when } from 'vitest-when' + +import { + useProtocolQuery, + useProtocolAnalysisAsDocumentQuery, +} from '@opentrons/react-api-client' +import { fixtureTiprack300ul } from '@opentrons/shared-data' +import { useRequiredProtocolLabware, useRunTimeParameters } from '../index' + +import type { UseQueryResult } from 'react-query' +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, +} from '@opentrons/shared-data' +import type { Protocol } from '@opentrons/api-client' + +vi.mock('@opentrons/react-api-client') +vi.mock('../../../../../organisms/Devices/hooks') + +const PROTOCOL_ID = 'fake_protocol_id' +const mockRTPData = [ + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'a dry run description', + type: 'bool', + default: false, + }, + { + displayName: 'Use Gripper', + variableName: 'USE_GRIPPER', + description: '', + type: 'bool', + default: true, + }, + { + displayName: 'Trash Tips', + variableName: 'TIP_TRASH', + description: 'throw tip in trash', + type: 'bool', + default: true, + }, + { + displayName: 'Deactivate Temperatures', + variableName: 'DEACTIVATE_TEMP', + description: 'deactivate temperature?', + type: 'bool', + default: true, + }, + { + displayName: 'Columns of Samples', + variableName: 'COLUMNS', + description: '', + suffix: 'mL', + type: 'int', + min: 1, + max: 14, + default: 4, + }, + { + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: '', + type: 'int', + min: 1, + max: 10, + default: 6, + }, + { + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '', + type: 'float', + min: 1.5, + max: 10.0, + default: 6.5, + }, + { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + description: '', + type: 'str', + choices: [ + { + displayName: 'no offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, +] +const mockLabwareDef = fixtureTiprack300ul as LabwareDefinition2 +const PROTOCOL_ANALYSIS = { + id: 'fake analysis', + status: 'completed', + labware: [], + pipettes: [{ id: 'pipId', pipetteName: 'p1000_multi_flex', mount: 'left' }], + modules: [ + { + id: 'modId', + model: 'heaterShakerModuleV1', + location: { slotName: 'D3' }, + serialNumber: 'serialNum', + }, + ], + commands: [ + { + key: 'CommandKey0', + commandType: 'loadModule', + params: { + model: 'heaterShakerModuleV1', + location: { slotName: 'D3' }, + }, + result: { + moduleId: 'modId', + }, + id: 'CommandId0', + status: 'succeeded', + error: null, + createdAt: 'fakeCreatedAtTimestamp', + startedAt: 'fakeStartedAtTimestamp', + completedAt: 'fakeCompletedAtTimestamp', + }, + { + key: 'CommandKey1', + commandType: 'loadLabware', + params: { + labwareId: 'firstLabwareId', + location: { moduleId: 'modId' }, + displayName: 'first labware nickname', + }, + result: { + labwareId: 'firstLabwareId', + definition: mockLabwareDef, + offset: { x: 0, y: 0, z: 0 }, + }, + id: 'CommandId1', + status: 'succeeded', + error: null, + createdAt: 'fakeCreatedAtTimestamp', + startedAt: 'fakeStartedAtTimestamp', + completedAt: 'fakeCompletedAtTimestamp', + }, + ], + runTimeParameters: mockRTPData, +} as any + +const NULL_COMMAND = { + id: '97ba49a5-04f6-4f91-986a-04a0eb632882', + createdAt: '2022-09-07T19:47:42.781065+00:00', + commandType: 'loadPipette', + key: '0feeecaf-3895-46d7-ab71-564601265e35', + status: 'succeeded', + params: { + pipetteName: 'p20_single_gen2', + mount: 'left', + pipetteId: '90183a18-a1df-4fd6-9636-be3bcec63fe4', + }, + result: { + pipetteId: '90183a18-a1df-4fd6-9636-be3bcec63fe4', + }, + startedAt: '2022-09-07T19:47:42.782665+00:00', + completedAt: '2022-09-07T19:47:42.785061+00:00', +} +const NULL_PROTOCOL_ANALYSIS = { + ...PROTOCOL_ANALYSIS, + id: 'null_analysis', + commands: [NULL_COMMAND], +} as any + +describe('useRunTimeParameters', () => { + beforeEach(() => { + when(vi.mocked(useProtocolQuery)) + .calledWith(PROTOCOL_ID) + .thenReturn({ + data: { + data: { analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id } as any] }, + }, + } as UseQueryResult) + when(vi.mocked(useProtocolAnalysisAsDocumentQuery)) + .calledWith(PROTOCOL_ID, PROTOCOL_ANALYSIS.id, { enabled: true }) + .thenReturn({ + data: PROTOCOL_ANALYSIS, + } as UseQueryResult) + }) + it('return RTP', () => { + const { result } = renderHook(() => useRunTimeParameters(PROTOCOL_ID)) + expect(result.current).toBe(mockRTPData) + }) +}) +describe('useRequiredProtocolLabware', () => { + beforeEach(() => { + when(vi.mocked(useProtocolQuery)) + .calledWith(PROTOCOL_ID) + .thenReturn({ + data: { + data: { analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id } as any] }, + }, + } as UseQueryResult) + when(vi.mocked(useProtocolAnalysisAsDocumentQuery)) + .calledWith(PROTOCOL_ID, PROTOCOL_ANALYSIS.id, { enabled: true }) + .thenReturn({ + data: PROTOCOL_ANALYSIS, + } as UseQueryResult) + when(vi.mocked(useProtocolAnalysisAsDocumentQuery)) + .calledWith(PROTOCOL_ID, NULL_PROTOCOL_ANALYSIS.id, { enabled: true }) + .thenReturn({ + data: NULL_PROTOCOL_ANALYSIS, + } as UseQueryResult) + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + it('should return LabwareSetupItem array', () => { + const { result } = renderHook(() => useRequiredProtocolLabware(PROTOCOL_ID)) + expect(result.current.length).toBe(1) + expect(result.current[0].nickName).toEqual('first labware nickname') + expect(result.current[0].definition.dimensions.xDimension).toBe(127.76) + expect(result.current[0].definition.metadata.displayName).toEqual( + '300ul Tiprack FIXTURE' + ) + }) + + it('should return empty array when there is no match with protocol id', () => { + when(vi.mocked(useProtocolQuery)) + .calledWith(PROTOCOL_ID) + .thenReturn({ + data: { + data: { + analysisSummaries: [{ id: NULL_PROTOCOL_ANALYSIS.id } as any], + }, + }, + } as UseQueryResult) + const { result } = renderHook(() => useRequiredProtocolLabware(PROTOCOL_ID)) + expect(result.current.length).toBe(0) + }) +}) diff --git a/app/src/pages/Desktop/Protocols/hooks/index.ts b/app/src/pages/Desktop/Protocols/hooks/index.ts new file mode 100644 index 00000000000..26941e94acd --- /dev/null +++ b/app/src/pages/Desktop/Protocols/hooks/index.ts @@ -0,0 +1,82 @@ +import last from 'lodash/last' +import { + useProtocolAnalysisAsDocumentQuery, + useProtocolQuery, +} from '@opentrons/react-api-client' + +import { + getLabwareSetupItemGroups, + useRequiredProtocolHardwareFromAnalysis, +} from '../../../../transformations/commands' + +import type { + CompletedProtocolAnalysis, + RunTimeParameter, +} from '@opentrons/shared-data' +import type { + LabwareSetupItem, + ProtocolHardware, +} from '../../../../transformations/commands' + +/** + * Returns an array of RunTimeParameters objects that are optional by the given protocol ID. + * + * @param {string} protocolId The ID of the protocol for which required hardware is being retrieved. + * @returns {RunTimeParameters[]} An array of RunTimeParameters objects that are required by the given protocol ID. + */ + +export const useRunTimeParameters = ( + protocolId: string +): RunTimeParameter[] => { + const { data: protocolData } = useProtocolQuery(protocolId) + const { data: analysis } = useProtocolAnalysisAsDocumentQuery( + protocolId, + last(protocolData?.data.analysisSummaries)?.id ?? null, + { enabled: protocolData != null } + ) + + return analysis?.runTimeParameters ?? [] +} + +/** + * Returns an array of ProtocolHardware objects that are required by the given protocol ID. + * + * @param {string} protocolId The ID of the protocol for which required hardware is being retrieved. + * @returns {ProtocolHardware[]} An array of ProtocolHardware objects that are required by the given protocol ID. + */ + +export const useRequiredProtocolHardware = ( + protocolId: string +): { requiredProtocolHardware: ProtocolHardware[]; isLoading: boolean } => { + const { data: protocolData } = useProtocolQuery(protocolId) + const { data: analysis } = useProtocolAnalysisAsDocumentQuery( + protocolId, + last(protocolData?.data.analysisSummaries)?.id ?? null, + { enabled: protocolData != null } + ) + + return useRequiredProtocolHardwareFromAnalysis(analysis ?? null) +} + +/** + * Returns an array of LabwareSetupItem objects that are required by the given protocol ID. + * + * @param {string} protocolId The ID of the protocol for which required labware setup items are being retrieved. + * @returns {LabwareSetupItem[]} An array of LabwareSetupItem objects that are required by the given protocol ID. + */ +export const useRequiredProtocolLabware = ( + protocolId: string +): LabwareSetupItem[] => { + const { data: protocolData } = useProtocolQuery(protocolId) + const { + data: mostRecentAnalysis, + } = useProtocolAnalysisAsDocumentQuery( + protocolId, + last(protocolData?.data.analysisSummaries)?.id ?? null, + { enabled: protocolData != null } + ) + const commands = + (mostRecentAnalysis as CompletedProtocolAnalysis)?.commands ?? [] + const { onDeckItems, offDeckItems } = getLabwareSetupItemGroups(commands) + return [...onDeckItems, ...offDeckItems] +} diff --git a/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx b/app/src/pages/ODD/ConnectViaEthernet/DisplayConnectionStatus.tsx similarity index 98% rename from app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx rename to app/src/pages/ODD/ConnectViaEthernet/DisplayConnectionStatus.tsx index 177599adb5b..c2632e05515 100644 --- a/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx +++ b/app/src/pages/ODD/ConnectViaEthernet/DisplayConnectionStatus.tsx @@ -16,7 +16,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { MediumButton } from '../../atoms/buttons' +import { MediumButton } from '../../../atoms/buttons' interface DisplayConnectionStatusProps { isConnected: boolean diff --git a/app/src/pages/ConnectViaEthernet/TitleHeader.tsx b/app/src/pages/ODD/ConnectViaEthernet/TitleHeader.tsx similarity index 100% rename from app/src/pages/ConnectViaEthernet/TitleHeader.tsx rename to app/src/pages/ODD/ConnectViaEthernet/TitleHeader.tsx diff --git a/app/src/pages/ConnectViaEthernet/__tests__/ConnectViaEthernet.test.tsx b/app/src/pages/ODD/ConnectViaEthernet/__tests__/ConnectViaEthernet.test.tsx similarity index 71% rename from app/src/pages/ConnectViaEthernet/__tests__/ConnectViaEthernet.test.tsx rename to app/src/pages/ODD/ConnectViaEthernet/__tests__/ConnectViaEthernet.test.tsx index 404f0c9f95c..8188371e4fd 100644 --- a/app/src/pages/ConnectViaEthernet/__tests__/ConnectViaEthernet.test.tsx +++ b/app/src/pages/ODD/ConnectViaEthernet/__tests__/ConnectViaEthernet.test.tsx @@ -2,16 +2,16 @@ import * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { vi, it, describe, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' -import * as Networking from '../../../redux/networking' -import { TitleHeader } from '../../../pages/ConnectViaEthernet/TitleHeader' -import { DisplayConnectionStatus } from '../../../pages/ConnectViaEthernet/DisplayConnectionStatus' -import { ConnectViaEthernet } from '../../../pages/ConnectViaEthernet' +import { i18n } from '../../../../i18n' +import * as Networking from '../../../../redux/networking' +import { TitleHeader } from '../TitleHeader' +import { DisplayConnectionStatus } from '../DisplayConnectionStatus' +import { ConnectViaEthernet } from '../' -vi.mock('../../../redux/networking') -vi.mock('../../../redux/discovery') +vi.mock('../../../../redux/networking') +vi.mock('../../../../redux/discovery') vi.mock('../TitleHeader') vi.mock('../DisplayConnectionStatus') diff --git a/app/src/pages/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx b/app/src/pages/ODD/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx similarity index 92% rename from app/src/pages/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx rename to app/src/pages/ODD/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx index 92864b2a1d0..94dd9ab4d48 100644 --- a/app/src/pages/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx +++ b/app/src/pages/ODD/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx @@ -2,9 +2,9 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { DisplayConnectionStatus } from '../../../pages/ConnectViaEthernet/DisplayConnectionStatus' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { DisplayConnectionStatus } from '../DisplayConnectionStatus' import type { NavigateFunction } from 'react-router-dom' diff --git a/app/src/pages/ConnectViaEthernet/__tests__/TitleHeader.test.tsx b/app/src/pages/ODD/ConnectViaEthernet/__tests__/TitleHeader.test.tsx similarity index 88% rename from app/src/pages/ConnectViaEthernet/__tests__/TitleHeader.test.tsx rename to app/src/pages/ODD/ConnectViaEthernet/__tests__/TitleHeader.test.tsx index 14575779736..a6bc8902fa7 100644 --- a/app/src/pages/ConnectViaEthernet/__tests__/TitleHeader.test.tsx +++ b/app/src/pages/ODD/ConnectViaEthernet/__tests__/TitleHeader.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' -import { TitleHeader } from '../../../pages/ConnectViaEthernet/TitleHeader' +import { renderWithProviders } from '../../../../__testing-utils__' +import { TitleHeader } from '../TitleHeader' import type { NavigateFunction } from 'react-router-dom' diff --git a/app/src/pages/ConnectViaEthernet/index.tsx b/app/src/pages/ODD/ConnectViaEthernet/index.tsx similarity index 79% rename from app/src/pages/ConnectViaEthernet/index.tsx rename to app/src/pages/ODD/ConnectViaEthernet/index.tsx index f7f386074d9..5cede84110f 100644 --- a/app/src/pages/ConnectViaEthernet/index.tsx +++ b/app/src/pages/ODD/ConnectViaEthernet/index.tsx @@ -9,14 +9,14 @@ import { DIRECTION_COLUMN, } from '@opentrons/components' -import { StepMeter } from '../../atoms/StepMeter' -import { NetworkDetailsModal } from '../../organisms/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal' -import { getNetworkInterfaces, fetchStatus } from '../../redux/networking' -import { getLocalRobot } from '../../redux/discovery' -import { TitleHeader } from '../../pages/ConnectViaEthernet/TitleHeader' -import { DisplayConnectionStatus } from '../../pages/ConnectViaEthernet/DisplayConnectionStatus' +import { StepMeter } from '../../../atoms/StepMeter' +import { NetworkDetailsModal } from '../../../organisms/ODD/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal' +import { getNetworkInterfaces, fetchStatus } from '../../../redux/networking' +import { getLocalRobot } from '../../../redux/discovery' +import { TitleHeader } from './TitleHeader' +import { DisplayConnectionStatus } from './DisplayConnectionStatus' -import type { State, Dispatch } from '../../redux/types' +import type { State, Dispatch } from '../../../redux/types' const STATUS_REFRESH_MS = 5000 diff --git a/app/src/pages/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx b/app/src/pages/ODD/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx similarity index 94% rename from app/src/pages/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx rename to app/src/pages/ODD/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx index 79647ac1fb8..2133f4388fa 100644 --- a/app/src/pages/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx +++ b/app/src/pages/ODD/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx @@ -3,11 +3,11 @@ import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { fireEvent } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { useConnectionsQuery } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' -import { ConnectViaUSB } from '../../../pages/ConnectViaUSB' +import { i18n } from '../../../../i18n' +import { ConnectViaUSB } from '../' import type { UseQueryResult } from 'react-query' import type { ActiveConnections } from '@opentrons/api-client' diff --git a/app/src/pages/ConnectViaUSB/index.tsx b/app/src/pages/ODD/ConnectViaUSB/index.tsx similarity index 98% rename from app/src/pages/ConnectViaUSB/index.tsx rename to app/src/pages/ODD/ConnectViaUSB/index.tsx index a802d36d891..86eb16124d6 100644 --- a/app/src/pages/ConnectViaUSB/index.tsx +++ b/app/src/pages/ODD/ConnectViaUSB/index.tsx @@ -18,8 +18,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { useConnectionsQuery } from '@opentrons/react-api-client' -import { StepMeter } from '../../atoms/StepMeter' -import { MediumButton } from '../../atoms/buttons' +import { StepMeter } from '../../../atoms/StepMeter' +import { MediumButton } from '../../../atoms/buttons' export function ConnectViaUSB(): JSX.Element { const { i18n, t } = useTranslation(['device_settings', 'shared', 'branded']) diff --git a/app/src/pages/ConnectViaWifi/JoinOtherNetwork.tsx b/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx similarity index 87% rename from app/src/pages/ConnectViaWifi/JoinOtherNetwork.tsx rename to app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx index 72a7315678c..25e663bd6a0 100644 --- a/app/src/pages/ConnectViaWifi/JoinOtherNetwork.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx @@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN } from '@opentrons/components' -import { SetWifiSsid } from '../../organisms/NetworkSettings' -import { RobotSetupHeader } from '../../organisms/RobotSetupHeader' +import { SetWifiSsid } from '../../../organisms/ODD/NetworkSettings' +import { RobotSetupHeader } from '../../../organisms/RobotSetupHeader' -import type { WifiScreenOption } from '../../pages/ConnectViaWifi' +import type { WifiScreenOption } from './' interface JoinOtherNetworkProps { setCurrentOption: (option: WifiScreenOption) => void diff --git a/app/src/pages/ConnectViaWifi/SelectAuthenticationType.tsx b/app/src/pages/ODD/ConnectViaWifi/SelectAuthenticationType.tsx similarity index 87% rename from app/src/pages/ConnectViaWifi/SelectAuthenticationType.tsx rename to app/src/pages/ODD/ConnectViaWifi/SelectAuthenticationType.tsx index 564b272d75d..2355383d1c1 100644 --- a/app/src/pages/ConnectViaWifi/SelectAuthenticationType.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/SelectAuthenticationType.tsx @@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN } from '@opentrons/components' -import { SelectAuthenticationType as SelectAuthenticationTypeComponent } from '../../organisms/NetworkSettings' -import { RobotSetupHeader } from '../../organisms/RobotSetupHeader' +import { SelectAuthenticationType as SelectAuthenticationTypeComponent } from '../../../organisms/ODD/NetworkSettings' +import { RobotSetupHeader } from '../../../organisms/RobotSetupHeader' import type { WifiSecurityType } from '@opentrons/api-client' -import type { WifiScreenOption } from '../../pages/ConnectViaWifi' +import type { WifiScreenOption } from './' interface SelectAuthenticationTypeProps { handleWifiConnect: () => void diff --git a/app/src/pages/ConnectViaWifi/SetWifiCred.tsx b/app/src/pages/ODD/ConnectViaWifi/SetWifiCred.tsx similarity index 80% rename from app/src/pages/ConnectViaWifi/SetWifiCred.tsx rename to app/src/pages/ODD/ConnectViaWifi/SetWifiCred.tsx index ebb0964b484..a1b76adb42a 100644 --- a/app/src/pages/ConnectViaWifi/SetWifiCred.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/SetWifiCred.tsx @@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN } from '@opentrons/components' -import { SetWifiCred as SetWifiCredComponent } from '../../organisms/NetworkSettings' -import { RobotSetupHeader } from '../../organisms/RobotSetupHeader' +import { SetWifiCred as SetWifiCredComponent } from '../../../organisms/ODD/NetworkSettings' +import { RobotSetupHeader } from '../../../organisms/RobotSetupHeader' -import type { WifiScreenOption } from '../../pages/ConnectViaWifi' +import type { WifiScreenOption } from './' interface SetWifiCredProps { handleConnect: () => void diff --git a/app/src/pages/ConnectViaWifi/WifiConnectStatus.tsx b/app/src/pages/ODD/ConnectViaWifi/WifiConnectStatus.tsx similarity index 87% rename from app/src/pages/ConnectViaWifi/WifiConnectStatus.tsx rename to app/src/pages/ODD/ConnectViaWifi/WifiConnectStatus.tsx index bbf336e8276..3bf7cca8e58 100644 --- a/app/src/pages/ConnectViaWifi/WifiConnectStatus.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/WifiConnectStatus.tsx @@ -7,13 +7,13 @@ import { ConnectingNetwork, FailedToConnect, WifiConnectionDetails, -} from '../../organisms/NetworkSettings' -import { RobotSetupHeader } from '../../organisms/RobotSetupHeader' -import * as RobotApi from '../../redux/robot-api' +} from '../../../organisms/ODD/NetworkSettings' +import { RobotSetupHeader } from '../../../organisms/RobotSetupHeader' +import * as RobotApi from '../../../redux/robot-api' import type { WifiSecurityType } from '@opentrons/api-client' -import type { RequestState } from '../../redux/robot-api/types' -import type { WifiScreenOption } from '../../pages/ConnectViaWifi' +import type { RequestState } from '../../../redux/robot-api/types' +import type { WifiScreenOption } from './' interface WifiConnectStatusProps { handleConnect: () => void diff --git a/app/src/pages/ConnectViaWifi/__tests__/ConnectViaWifi.test.tsx b/app/src/pages/ODD/ConnectViaWifi/__tests__/ConnectViaWifi.test.tsx similarity index 88% rename from app/src/pages/ConnectViaWifi/__tests__/ConnectViaWifi.test.tsx rename to app/src/pages/ODD/ConnectViaWifi/__tests__/ConnectViaWifi.test.tsx index cc9cc7f5275..771a9264587 100644 --- a/app/src/pages/ConnectViaWifi/__tests__/ConnectViaWifi.test.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/__tests__/ConnectViaWifi.test.tsx @@ -3,18 +3,18 @@ import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import * as RobotApi from '../../../redux/robot-api' -import * as Fixtures from '../../../redux/networking/__fixtures__' -import { useWifiList } from '../../../resources/networking/hooks' -import * as Networking from '../../../redux/networking' -import { ConnectViaWifi } from '../../../pages/ConnectViaWifi' - -vi.mock('../../../redux/discovery') -vi.mock('../../../resources/networking/hooks') -vi.mock('../../../redux/networking/selectors') -vi.mock('../../../redux/robot-api/selectors') +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import * as RobotApi from '../../../../redux/robot-api' +import * as Fixtures from '../../../../redux/networking/__fixtures__' +import { useWifiList } from '../../../../resources/networking/hooks' +import * as Networking from '../../../../redux/networking' +import { ConnectViaWifi } from '../' + +vi.mock('../../../../redux/discovery') +vi.mock('../../../../resources/networking/hooks') +vi.mock('../../../../redux/networking/selectors') +vi.mock('../../../../redux/robot-api/selectors') const mockWifiList = [ { ...Fixtures.mockWifiNetwork, ssid: 'foo', active: true }, diff --git a/app/src/pages/ConnectViaWifi/index.tsx b/app/src/pages/ODD/ConnectViaWifi/index.tsx similarity index 82% rename from app/src/pages/ConnectViaWifi/index.tsx rename to app/src/pages/ODD/ConnectViaWifi/index.tsx index 4530503a69c..2207b17e386 100644 --- a/app/src/pages/ConnectViaWifi/index.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/index.tsx @@ -4,19 +4,19 @@ import last from 'lodash/last' import { Flex, DIRECTION_COLUMN, SPACING } from '@opentrons/components' -import { StepMeter } from '../../atoms/StepMeter' -import { DisplayWifiList } from '../../organisms/NetworkSettings' -import * as Networking from '../../redux/networking' -import { getLocalRobot } from '../../redux/discovery' -import * as RobotApi from '../../redux/robot-api' -import { useWifiList } from '../../resources/networking/hooks' -import { JoinOtherNetwork } from '../../pages/ConnectViaWifi/JoinOtherNetwork' -import { SelectAuthenticationType } from '../../pages/ConnectViaWifi/SelectAuthenticationType' -import { SetWifiCred } from '../../pages/ConnectViaWifi/SetWifiCred' -import { WifiConnectStatus } from '../../pages/ConnectViaWifi/WifiConnectStatus' +import { StepMeter } from '../../../atoms/StepMeter' +import { DisplayWifiList } from '../../../organisms/ODD/NetworkSettings' +import * as Networking from '../../../redux/networking' +import { getLocalRobot } from '../../../redux/discovery' +import * as RobotApi from '../../../redux/robot-api' +import { useWifiList } from '../../../resources/networking/hooks' +import { JoinOtherNetwork } from './JoinOtherNetwork' +import { SelectAuthenticationType } from './SelectAuthenticationType' +import { SetWifiCred } from './SetWifiCred' +import { WifiConnectStatus } from './WifiConnectStatus' import type { WifiSecurityType } from '@opentrons/api-client' -import type { State } from '../../redux/types' +import type { State } from '../../../redux/types' const WIFI_LIST_POLL_MS = 5000 export type WifiScreenOption = diff --git a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx b/app/src/pages/ODD/DeckConfiguration/__tests__/DeckConfiguration.test.tsx similarity index 82% rename from app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx rename to app/src/pages/ODD/DeckConfiguration/__tests__/DeckConfiguration.test.tsx index 1e34e917eca..4fd7159c028 100644 --- a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx +++ b/app/src/pages/ODD/DeckConfiguration/__tests__/DeckConfiguration.test.tsx @@ -4,18 +4,18 @@ import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { DeckConfigurator } from '@opentrons/components' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data' -import { i18n } from '../../../i18n' -import { DeckFixtureSetupInstructionsModal } from '../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' +import { i18n } from '../../../../i18n' +import { DeckFixtureSetupInstructionsModal } from '../../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' import { DeckConfigurationEditor } from '..' import { useNotifyDeckConfigurationQuery, useDeckConfigurationEditingTools, -} from '../../../resources/deck_configuration' +} from '../../../../resources/deck_configuration' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' @@ -48,12 +48,12 @@ const mockDeckConfig = [ vi.mock('@opentrons/react-api-client') vi.mock( - '../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' + '../../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' ) vi.mock( - '../../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' + '../../../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' ) -vi.mock('../../../resources/deck_configuration') +vi.mock('../../../../resources/deck_configuration') const render = () => { return renderWithProviders( diff --git a/app/src/pages/DeckConfiguration/index.tsx b/app/src/pages/ODD/DeckConfiguration/index.tsx similarity index 83% rename from app/src/pages/DeckConfiguration/index.tsx rename to app/src/pages/ODD/DeckConfiguration/index.tsx index ef27e81bc0a..ebb836dd750 100644 --- a/app/src/pages/DeckConfiguration/index.tsx +++ b/app/src/pages/ODD/DeckConfiguration/index.tsx @@ -11,16 +11,16 @@ import { JUSTIFY_SPACE_AROUND, } from '@opentrons/components' -import { ChildNavigation } from '../../organisms/ChildNavigation' -import { DeckFixtureSetupInstructionsModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' -import { DeckConfigurationDiscardChangesModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' -import { getTopPortalEl } from '../../App/portal' +import { ChildNavigation } from '../../../organisms/ChildNavigation' +import { DeckFixtureSetupInstructionsModal } from '../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' +import { DeckConfigurationDiscardChangesModal } from '../../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' +import { getTopPortalEl } from '../../../App/portal' import { useDeckConfigurationEditingTools, useNotifyDeckConfigurationQuery, -} from '../../resources/deck_configuration' +} from '../../../resources/deck_configuration' -import type { SmallButton } from '../../atoms/buttons' +import type { SmallButton } from '../../../atoms/buttons' export function DeckConfigurationEditor(): JSX.Element { const { t, i18n } = useTranslation([ diff --git a/app/src/pages/EmergencyStop/__tests__/EmergencyStop.test.tsx b/app/src/pages/ODD/EmergencyStop/__tests__/EmergencyStop.test.tsx similarity index 96% rename from app/src/pages/EmergencyStop/__tests__/EmergencyStop.test.tsx rename to app/src/pages/ODD/EmergencyStop/__tests__/EmergencyStop.test.tsx index 6e34f86c218..9914995a321 100644 --- a/app/src/pages/EmergencyStop/__tests__/EmergencyStop.test.tsx +++ b/app/src/pages/ODD/EmergencyStop/__tests__/EmergencyStop.test.tsx @@ -3,9 +3,9 @@ import { vi, it, describe, expect, beforeEach } from 'vitest' import { useEstopQuery } from '@opentrons/react-api-client' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { EmergencyStop } from '..' import type { NavigateFunction } from 'react-router-dom' diff --git a/app/src/pages/EmergencyStop/index.tsx b/app/src/pages/ODD/EmergencyStop/index.tsx similarity index 94% rename from app/src/pages/EmergencyStop/index.tsx rename to app/src/pages/ODD/EmergencyStop/index.tsx index b5419ec5790..c94c8e112eb 100644 --- a/app/src/pages/EmergencyStop/index.tsx +++ b/app/src/pages/ODD/EmergencyStop/index.tsx @@ -16,10 +16,10 @@ import { } from '@opentrons/components' import { useEstopQuery } from '@opentrons/react-api-client' -import { MediumButton } from '../../atoms/buttons' -import { StepMeter } from '../../atoms/StepMeter' +import { MediumButton } from '../../../atoms/buttons' +import { StepMeter } from '../../../atoms/StepMeter' -import estopImg from '../../assets/images/on-device-display/install_e_stop.png' +import estopImg from '../../../assets/images/on-device-display/install_e_stop.png' const ESTOP_STATUS_REFETCH_INTERVAL_MS = 10000 diff --git a/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx b/app/src/pages/ODD/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx similarity index 82% rename from app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx rename to app/src/pages/ODD/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx index a7e9076bb63..2c5ea7382e0 100644 --- a/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx +++ b/app/src/pages/ODD/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx @@ -4,9 +4,9 @@ import { screen } from '@testing-library/react' import { useRobotSettingsQuery } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { getIsShellReady } from '../../../redux/shell' +import { getIsShellReady } from '../../../../redux/shell' import { InitialLoadingScreen } from '..' @@ -14,8 +14,8 @@ import type { UseQueryResult } from 'react-query' import type { RobotSettingsResponse } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') -vi.mock('../../../redux/config') -vi.mock('../../../redux/shell') +vi.mock('../../../../redux/config') +vi.mock('../../../../redux/shell') const render = () => { return renderWithProviders() diff --git a/app/src/pages/InitialLoadingScreen/index.tsx b/app/src/pages/ODD/InitialLoadingScreen/index.tsx similarity index 95% rename from app/src/pages/InitialLoadingScreen/index.tsx rename to app/src/pages/ODD/InitialLoadingScreen/index.tsx index d57519bfa3b..4d4ef1d5503 100644 --- a/app/src/pages/InitialLoadingScreen/index.tsx +++ b/app/src/pages/ODD/InitialLoadingScreen/index.tsx @@ -10,7 +10,7 @@ import { SPACING, } from '@opentrons/components' import { useRobotSettingsQuery } from '@opentrons/react-api-client' -import { getIsShellReady } from '../../redux/shell' +import { getIsShellReady } from '../../../redux/shell' export function InitialLoadingScreen({ children, diff --git a/app/src/pages/InstrumentDetail/InstrumentDetailOverflowMenu.tsx b/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx similarity index 93% rename from app/src/pages/InstrumentDetail/InstrumentDetailOverflowMenu.tsx rename to app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx index 494e0f3d18c..e21ff66864f 100644 --- a/app/src/pages/InstrumentDetail/InstrumentDetailOverflowMenu.tsx +++ b/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx @@ -20,14 +20,14 @@ import { } from '@opentrons/shared-data' import { ApiHostProvider } from '@opentrons/react-api-client' -import { PipetteWizardFlows } from '../../organisms/PipetteWizardFlows' -import { GripperWizardFlows } from '../../organisms/GripperWizardFlows' +import { PipetteWizardFlows } from '../../../organisms/PipetteWizardFlows' +import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows' import { DropTipWizardFlows, useDropTipWizardFlows, -} from '../../organisms/DropTipWizardFlows' -import { FLOWS } from '../../organisms/PipetteWizardFlows/constants' -import { GRIPPER_FLOW_TYPES } from '../../organisms/GripperWizardFlows/constants' +} from '../../../organisms/DropTipWizardFlows' +import { FLOWS } from '../../../organisms/PipetteWizardFlows/constants' +import { GRIPPER_FLOW_TYPES } from '../../../organisms/GripperWizardFlows/constants' import type { PipetteData, diff --git a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx b/app/src/pages/ODD/InstrumentDetail/__tests__/InstrumentDetail.test.tsx similarity index 93% rename from app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx rename to app/src/pages/ODD/InstrumentDetail/__tests__/InstrumentDetail.test.tsx index 6b92a5ab9be..7baada93f77 100644 --- a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx +++ b/app/src/pages/ODD/InstrumentDetail/__tests__/InstrumentDetail.test.tsx @@ -4,15 +4,15 @@ import { screen } from '@testing-library/react' import { useParams } from 'react-router-dom' import { useInstrumentsQuery } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { InstrumentDetail } from '../../../pages/InstrumentDetail' +import { i18n } from '../../../../i18n' +import { InstrumentDetail } from '..' import { useGripperDisplayName, usePipetteModelSpecs, -} from '../../../resources/instruments/hooks' -import { useIsOEMMode } from '../../../resources/robot-settings/hooks' +} from '../../../../resources/instruments/hooks' +import { useIsOEMMode } from '../../../../resources/robot-settings/hooks' import type { Instruments } from '@opentrons/api-client' @@ -21,8 +21,8 @@ vi.mock('react-router-dom', () => ({ useParams: vi.fn(), useNavigate: vi.fn(), })) -vi.mock('../../../resources/instruments/hooks') -vi.mock('../../../resources/robot-settings/hooks') +vi.mock('../../../../resources/instruments/hooks') +vi.mock('../../../../resources/robot-settings/hooks') const render = () => { return renderWithProviders(, { diff --git a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx b/app/src/pages/ODD/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx similarity index 89% rename from app/src/pages/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx rename to app/src/pages/ODD/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx index 2c2c534ada3..a60ec5e2f4e 100644 --- a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx +++ b/app/src/pages/ODD/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx @@ -3,15 +3,15 @@ import NiceModal from '@ebay/nice-modal-react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { handleInstrumentDetailOverflowMenu } from '../InstrumentDetailOverflowMenu' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' -import { PipetteWizardFlows } from '../../../organisms/PipetteWizardFlows' -import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows' -import { useDropTipWizardFlows } from '../../../organisms/DropTipWizardFlows' +import { useNotifyCurrentMaintenanceRun } from '../../../../resources/maintenance_runs' +import { PipetteWizardFlows } from '../../../../organisms/PipetteWizardFlows' +import { GripperWizardFlows } from '../../../../organisms/GripperWizardFlows' +import { useDropTipWizardFlows } from '../../../../organisms/DropTipWizardFlows' import type { Mock } from 'vitest' import type { @@ -28,10 +28,10 @@ vi.mock('@opentrons/shared-data', async importOriginal => { getPipetteModelSpecs: vi.fn(), } }) -vi.mock('../../../resources/maintenance_runs') -vi.mock('../../../organisms/PipetteWizardFlows') -vi.mock('../../../organisms/GripperWizardFlows') -vi.mock('../../../organisms/DropTipWizardFlows') +vi.mock('../../../../resources/maintenance_runs') +vi.mock('../../../../organisms/PipetteWizardFlows') +vi.mock('../../../../organisms/GripperWizardFlows') +vi.mock('../../../../organisms/DropTipWizardFlows') const MOCK_PIPETTE = { mount: 'left', diff --git a/app/src/pages/InstrumentDetail/index.tsx b/app/src/pages/ODD/InstrumentDetail/index.tsx similarity index 88% rename from app/src/pages/InstrumentDetail/index.tsx rename to app/src/pages/ODD/InstrumentDetail/index.tsx index 3f606ba3432..fae2d418c59 100644 --- a/app/src/pages/InstrumentDetail/index.tsx +++ b/app/src/pages/ODD/InstrumentDetail/index.tsx @@ -14,14 +14,14 @@ import { JUSTIFY_SPACE_BETWEEN, } from '@opentrons/components' -import { BackButton } from '../../atoms/buttons/BackButton' -import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants' -import { InstrumentInfo } from '../../organisms/InstrumentInfo' -import { handleInstrumentDetailOverflowMenu } from '../../pages/InstrumentDetail/InstrumentDetailOverflowMenu' +import { BackButton } from '../../../atoms/buttons/BackButton' +import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons/constants' +import { InstrumentInfo } from '../../../organisms/InstrumentInfo' +import { handleInstrumentDetailOverflowMenu } from './InstrumentDetailOverflowMenu' import { useGripperDisplayName, usePipetteModelSpecs, -} from '../../resources/instruments/hooks' +} from '../../../resources/instruments/hooks' import type { GripperData, PipetteData } from '@opentrons/api-client' import type { GripperModel, PipetteModel } from '@opentrons/shared-data' diff --git a/app/src/pages/InstrumentsDashboard/PipetteRecalibrationODDWarning.tsx b/app/src/pages/ODD/InstrumentsDashboard/PipetteRecalibrationODDWarning.tsx similarity index 100% rename from app/src/pages/InstrumentsDashboard/PipetteRecalibrationODDWarning.tsx rename to app/src/pages/ODD/InstrumentsDashboard/PipetteRecalibrationODDWarning.tsx diff --git a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx b/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx similarity index 89% rename from app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx rename to app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx index 18b6d2cfb9c..b544f4a9e60 100644 --- a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx +++ b/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx @@ -1,16 +1,16 @@ import * as React from 'react' import { Route, MemoryRouter, Routes } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { vi, describe, it, afterEach, beforeEach, expect } from 'vitest' import { useInstrumentsQuery } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' -import { ChoosePipette } from '../../../organisms/PipetteWizardFlows/ChoosePipette' -import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows' +import { i18n } from '../../../../i18n' +import { ChoosePipette } from '../../../../organisms/PipetteWizardFlows/ChoosePipette' +import { GripperWizardFlows } from '../../../../organisms/GripperWizardFlows' import { InstrumentsDashboard } from '..' -import { formatTimeWithUtcLabel } from '../../../resources/runs' -import { InstrumentDetail } from '../../../pages/InstrumentDetail' +import { formatTimeWithUtcLabel } from '../../../../resources/runs' +import { InstrumentDetail } from '../../InstrumentDetail' import type * as ReactApiClient from '@opentrons/react-api-client' const mockGripperData = { @@ -88,10 +88,10 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { ), } }) -vi.mock('../../../organisms/GripperWizardFlows') -vi.mock('../../../organisms/PipetteWizardFlows') -vi.mock('../../../organisms/PipetteWizardFlows/ChoosePipette') -vi.mock('../../../organisms/Navigation') +vi.mock('../../../../organisms/GripperWizardFlows') +vi.mock('../../../../organisms/PipetteWizardFlows') +vi.mock('../../../../organisms/PipetteWizardFlows/ChoosePipette') +vi.mock('../../../../organisms/Navigation') const render = (path = '/') => { return renderWithProviders( diff --git a/app/src/pages/InstrumentsDashboard/__tests__/PipetteRecalibrationODDWarning.test.tsx b/app/src/pages/ODD/InstrumentsDashboard/__tests__/PipetteRecalibrationODDWarning.test.tsx similarity index 86% rename from app/src/pages/InstrumentsDashboard/__tests__/PipetteRecalibrationODDWarning.test.tsx rename to app/src/pages/ODD/InstrumentsDashboard/__tests__/PipetteRecalibrationODDWarning.test.tsx index 8961d30b219..c70156cec5f 100644 --- a/app/src/pages/InstrumentsDashboard/__tests__/PipetteRecalibrationODDWarning.test.tsx +++ b/app/src/pages/ODD/InstrumentsDashboard/__tests__/PipetteRecalibrationODDWarning.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { screen } from '@testing-library/react' import { describe, it } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { PipetteRecalibrationODDWarning } from '../PipetteRecalibrationODDWarning' diff --git a/app/src/pages/InstrumentsDashboard/index.tsx b/app/src/pages/ODD/InstrumentsDashboard/index.tsx similarity index 87% rename from app/src/pages/InstrumentsDashboard/index.tsx rename to app/src/pages/ODD/InstrumentsDashboard/index.tsx index 46154442cb2..8595777f4b3 100644 --- a/app/src/pages/InstrumentsDashboard/index.tsx +++ b/app/src/pages/ODD/InstrumentsDashboard/index.tsx @@ -1,11 +1,11 @@ import * as React from 'react' import { useInstrumentsQuery } from '@opentrons/react-api-client' import { DIRECTION_COLUMN, Flex, SPACING } from '@opentrons/components' -import { PipetteWizardFlows } from '../../organisms/PipetteWizardFlows' -import { Navigation } from '../../organisms/Navigation' -import { AttachedInstrumentMountItem } from '../../organisms/InstrumentMountItem' -import { GripperWizardFlows } from '../../organisms/GripperWizardFlows' -import { getShowPipetteCalibrationWarning } from '../../organisms/Devices/utils' +import { PipetteWizardFlows } from '../../../organisms/PipetteWizardFlows' +import { Navigation } from '../../../organisms/Navigation' +import { AttachedInstrumentMountItem } from '../../../organisms/InstrumentMountItem' +import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows' +import { getShowPipetteCalibrationWarning } from '../../../organisms/Devices/utils' import { PipetteRecalibrationODDWarning } from './PipetteRecalibrationODDWarning' import type { GripperData, PipetteData } from '@opentrons/api-client' diff --git a/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx b/app/src/pages/ODD/NameRobot/__tests__/NameRobot.test.tsx similarity index 90% rename from app/src/pages/NameRobot/__tests__/NameRobot.test.tsx rename to app/src/pages/ODD/NameRobot/__tests__/NameRobot.test.tsx index a91c108a527..0605da1b768 100644 --- a/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx +++ b/app/src/pages/ODD/NameRobot/__tests__/NameRobot.test.tsx @@ -3,28 +3,28 @@ import { vi, it, describe, expect, beforeEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen, waitFor } from '@testing-library/react' -import { i18n } from '../../../i18n' -import { renderWithProviders } from '../../../__testing-utils__' -import { useTrackEvent } from '../../../redux/analytics' +import { i18n } from '../../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { useTrackEvent } from '../../../../redux/analytics' import { getConnectableRobots, getReachableRobots, getUnreachableRobots, -} from '../../../redux/discovery' -import { useIsUnboxingFlowOngoing } from '../../../organisms/RobotSettingsDashboard/NetworkSettings/hooks' +} from '../../../../redux/discovery' +import { useIsUnboxingFlowOngoing } from '../../../../organisms/ODD/hooks' import { mockConnectableRobot, mockReachableRobot, mockUnreachableRobot, -} from '../../../redux/discovery/__fixtures__' +} from '../../../../redux/discovery/__fixtures__' import { NameRobot } from '..' import type { NavigateFunction } from 'react-router-dom' -vi.mock('../../../redux/discovery/selectors') -vi.mock('../../../redux/config') -vi.mock('../../../redux/analytics') -vi.mock('../../../organisms/RobotSettingsDashboard/NetworkSettings/hooks') +vi.mock('../../../../redux/discovery/selectors') +vi.mock('../../../../redux/config') +vi.mock('../../../../redux/analytics') +vi.mock('../../../../organisms/ODD/hooks') const mockNavigate = vi.fn() diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/ODD/NameRobot/index.tsx similarity index 94% rename from app/src/pages/NameRobot/index.tsx rename to app/src/pages/ODD/NameRobot/index.tsx index 7e8be33383b..a2ba5a918c0 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/ODD/NameRobot/index.tsx @@ -30,17 +30,17 @@ import { getReachableRobots, getUnreachableRobots, getLocalRobot, -} from '../../redux/discovery' -import { useTrackEvent, ANALYTICS_RENAME_ROBOT } from '../../redux/analytics' -import { AlphanumericKeyboard } from '../../atoms/SoftwareKeyboard' -import { SmallButton } from '../../atoms/buttons' -import { StepMeter } from '../../atoms/StepMeter' -import { useIsUnboxingFlowOngoing } from '../../organisms/RobotSettingsDashboard/NetworkSettings/hooks' -import { ConfirmRobotName } from '../../organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName' +} from '../../../redux/discovery' +import { useTrackEvent, ANALYTICS_RENAME_ROBOT } from '../../../redux/analytics' +import { AlphanumericKeyboard } from '../../../atoms/SoftwareKeyboard' +import { SmallButton } from '../../../atoms/buttons' +import { StepMeter } from '../../../atoms/StepMeter' +import { useIsUnboxingFlowOngoing } from '../../../organisms/ODD/hooks' +import { ConfirmRobotName } from '../../../organisms/ODD/NameRobot/ConfirmRobotName' import type { FieldError, Resolver } from 'react-hook-form' import type { UpdatedRobotName } from '@opentrons/api-client' -import type { State, Dispatch } from '../../redux/types' +import type { State, Dispatch } from '../../../redux/types' interface FormValues { newRobotName: string diff --git a/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx b/app/src/pages/ODD/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx similarity index 94% rename from app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx rename to app/src/pages/ODD/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx index 73f9312cc3c..90bbf011997 100644 --- a/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx +++ b/app/src/pages/ODD/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx @@ -2,9 +2,9 @@ import * as React from 'react' import { vi, it, describe, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { NetworkSetupMenu } from '..' import type { NavigateFunction } from 'react-router-dom' diff --git a/app/src/pages/NetworkSetupMenu/index.tsx b/app/src/pages/ODD/NetworkSetupMenu/index.tsx similarity index 95% rename from app/src/pages/NetworkSetupMenu/index.tsx rename to app/src/pages/ODD/NetworkSetupMenu/index.tsx index 293107b7505..f2b6a0caadd 100644 --- a/app/src/pages/NetworkSetupMenu/index.tsx +++ b/app/src/pages/ODD/NetworkSetupMenu/index.tsx @@ -13,8 +13,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { StepMeter } from '../../atoms/StepMeter' -import { CardButton } from '../../molecules/CardButton' +import { StepMeter } from '../../../atoms/StepMeter' +import { CardButton } from '../../../molecules/CardButton' import type { IconName } from '@opentrons/components' diff --git a/app/src/pages/ProtocolDashboard/DeleteProtocolConfirmationModal.tsx b/app/src/pages/ODD/ProtocolDashboard/DeleteProtocolConfirmationModal.tsx similarity index 94% rename from app/src/pages/ProtocolDashboard/DeleteProtocolConfirmationModal.tsx rename to app/src/pages/ODD/ProtocolDashboard/DeleteProtocolConfirmationModal.tsx index b9abf223247..b09b9fcc394 100644 --- a/app/src/pages/ProtocolDashboard/DeleteProtocolConfirmationModal.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/DeleteProtocolConfirmationModal.tsx @@ -17,11 +17,11 @@ import { } from '@opentrons/components' import { useHost, useProtocolQuery } from '@opentrons/react-api-client' -import { SmallButton } from '../../atoms/buttons' -import { OddModal } from '../../molecules/OddModal' -import { useToaster } from '../../organisms/ToasterOven' +import { SmallButton } from '../../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' +import { useToaster } from '../../../organisms/ToasterOven' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' interface DeleteProtocolConfirmationModalProps { protocolId: string diff --git a/app/src/pages/ProtocolDashboard/LongPressModal.tsx b/app/src/pages/ODD/ProtocolDashboard/LongPressModal.tsx similarity index 93% rename from app/src/pages/ProtocolDashboard/LongPressModal.tsx rename to app/src/pages/ODD/ProtocolDashboard/LongPressModal.tsx index 5b098a1beb9..71a6ad16914 100644 --- a/app/src/pages/ProtocolDashboard/LongPressModal.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/LongPressModal.tsx @@ -12,13 +12,13 @@ import { } from '@opentrons/components' import { useCreateRunMutation } from '@opentrons/react-api-client' -import { MAXIMUM_PINNED_PROTOCOLS } from '../../App/constants' -import { SmallModalChildren } from '../../molecules/OddModal' -import { useToaster } from '../../organisms/ToasterOven' -import { getPinnedProtocolIds, updateConfigValue } from '../../redux/config' +import { MAXIMUM_PINNED_PROTOCOLS } from '../../../App/constants' +import { SmallModalChildren } from '../../../molecules/OddModal' +import { useToaster } from '../../../organisms/ToasterOven' +import { getPinnedProtocolIds, updateConfigValue } from '../../../redux/config' import type { UseLongPressResult } from '@opentrons/components' -import type { Dispatch } from '../../redux/types' +import type { Dispatch } from '../../../redux/types' interface LongPressModalProps { longpress: UseLongPressResult diff --git a/app/src/pages/ProtocolDashboard/NoProtocols.tsx b/app/src/pages/ODD/ProtocolDashboard/NoProtocols.tsx similarity index 92% rename from app/src/pages/ProtocolDashboard/NoProtocols.tsx rename to app/src/pages/ODD/ProtocolDashboard/NoProtocols.tsx index dd12382e38f..770ecc7c78c 100644 --- a/app/src/pages/ProtocolDashboard/NoProtocols.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/NoProtocols.tsx @@ -13,7 +13,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import imgSrc from '../../assets/images/on-device-display/empty_protocol_dashboard.png' +import imgSrc from '../../../assets/images/on-device-display/empty_protocol_dashboard.png' export function NoProtocols(): JSX.Element { const { t } = useTranslation(['protocol_info', 'branded']) diff --git a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx similarity index 98% rename from app/src/pages/ProtocolDashboard/PinnedProtocol.tsx rename to app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx index 341d8c856c1..8a1826ed62f 100644 --- a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx @@ -21,7 +21,7 @@ import { } from '@opentrons/components' import { LongPressModal } from './LongPressModal' -import { formatTimeWithUtcLabel } from '../../resources/runs' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' diff --git a/app/src/pages/ProtocolDashboard/PinnedProtocolCarousel.tsx b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocolCarousel.tsx similarity index 97% rename from app/src/pages/ProtocolDashboard/PinnedProtocolCarousel.tsx rename to app/src/pages/ODD/ProtocolDashboard/PinnedProtocolCarousel.tsx index 529378a95f5..9a631c65210 100644 --- a/app/src/pages/ProtocolDashboard/PinnedProtocolCarousel.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocolCarousel.tsx @@ -6,7 +6,7 @@ import { SPACING, } from '@opentrons/components' -import { useNotifyAllRunsQuery } from '../../resources/runs' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import { PinnedProtocol } from './PinnedProtocol' import type { ProtocolResource } from '@opentrons/shared-data' diff --git a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx b/app/src/pages/ODD/ProtocolDashboard/ProtocolCard.tsx similarity index 93% rename from app/src/pages/ProtocolDashboard/ProtocolCard.tsx rename to app/src/pages/ODD/ProtocolDashboard/ProtocolCard.tsx index 64aff2cac97..0e4e5a20de2 100644 --- a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/ProtocolCard.tsx @@ -29,14 +29,14 @@ import { } from '@opentrons/react-api-client' import { deleteProtocol, deleteRun, getProtocol } from '@opentrons/api-client' -import { SmallButton } from '../../atoms/buttons' -import { OddModal } from '../../molecules/OddModal' +import { SmallButton } from '../../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' import { LongPressModal } from './LongPressModal' -import { formatTimeWithUtcLabel } from '../../resources/runs' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' const REFETCH_INTERVAL = 5000 @@ -194,6 +194,15 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { if (isFailedAnalysis) protocolCardBackgroundColor = COLORS.red35 if (isRequiredCSV) protocolCardBackgroundColor = COLORS.yellow35 + const textWrap = (lastRun?: string): string => { + if (lastRun != null) { + lastRun = formatDistance(new Date(lastRun), new Date(), { + addSuffix: true, + }).replace('about ', '') + } + return lastRun === 'less than a minute ago' ? 'normal' : 'nowrap' + } + return ( - + {lastRun != null ? formatDistance(new Date(lastRun), new Date(), { addSuffix: true, diff --git a/app/src/pages/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx similarity index 92% rename from app/src/pages/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx rename to app/src/pages/ODD/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx index c12dcfa21fd..987b328a998 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx @@ -4,17 +4,17 @@ import { when } from 'vitest-when' import { act, fireEvent, screen } from '@testing-library/react' import { getProtocol, deleteProtocol, deleteRun } from '@opentrons/api-client' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { useHost, useProtocolQuery } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' -import { useToaster } from '../../../organisms/ToasterOven' +import { i18n } from '../../../../i18n' +import { useToaster } from '../../../../organisms/ToasterOven' import { DeleteProtocolConfirmationModal } from '../DeleteProtocolConfirmationModal' import type { HostConfig } from '@opentrons/api-client' vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') -vi.mock('../../../organisms/ToasterOven') +vi.mock('../../../../organisms/ToasterOven') const mockFunc = vi.fn() const PROTOCOL_ID = 'mockProtocolId' diff --git a/app/src/pages/ProtocolDashboard/__tests__/LongPressModal.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/LongPressModal.test.tsx similarity index 95% rename from app/src/pages/ProtocolDashboard/__tests__/LongPressModal.test.tsx rename to app/src/pages/ODD/ProtocolDashboard/__tests__/LongPressModal.test.tsx index ad3aa7bd2f5..3b5f0aaf912 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/LongPressModal.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/LongPressModal.test.tsx @@ -7,8 +7,8 @@ import { fireEvent, renderHook, screen } from '@testing-library/react' import { useLongPress } from '@opentrons/components' import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { LongPressModal } from '../LongPressModal' import type { HostConfig } from '@opentrons/api-client' diff --git a/app/src/pages/ProtocolDashboard/__tests__/NoProtocols.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/NoProtocols.test.tsx similarity index 86% rename from app/src/pages/ProtocolDashboard/__tests__/NoProtocols.test.tsx rename to app/src/pages/ODD/ProtocolDashboard/__tests__/NoProtocols.test.tsx index cf0f0738248..0f4a8fbdd57 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/NoProtocols.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/NoProtocols.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { describe, it, expect } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { NoProtocols } from '../NoProtocols' import { screen } from '@testing-library/react' diff --git a/app/src/pages/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx similarity index 96% rename from app/src/pages/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx rename to app/src/pages/ODD/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx index 15cc38baefe..fd363bf1f77 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx @@ -5,9 +5,9 @@ import { MemoryRouter } from 'react-router-dom' import { COLORS, TYPOGRAPHY } from '@opentrons/components' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useFeatureFlag } from '../../../redux/config' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { useFeatureFlag } from '../../../../redux/config' import { PinnedProtocol } from '../PinnedProtocol' import type { Chip } from '@opentrons/components' @@ -30,7 +30,7 @@ vi.mock('@opentrons/components', async importOriginal => { Chip: () =>
mock Chip
, } }) -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') const mockProtocol: ProtocolResource = { id: 'mockProtocol1', diff --git a/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/ProtocolCard.test.tsx similarity index 97% rename from app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx rename to app/src/pages/ODD/ProtocolDashboard/__tests__/ProtocolCard.test.tsx index 0e792cb8d70..2c783adcd37 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/ProtocolCard.test.tsx @@ -9,9 +9,9 @@ import { useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useFeatureFlag } from '../../../redux/config' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { useFeatureFlag } from '../../../../redux/config' import { ProtocolCard } from '../ProtocolCard' import type { NavigateFunction } from 'react-router-dom' @@ -32,7 +32,7 @@ vi.mock('react-router-dom', async importOriginal => { } }) vi.mock('@opentrons/react-api-client') -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') vi.mock('@opentrons/components', async importOriginal => { const actual = await importOriginal() return { diff --git a/app/src/pages/ProtocolDashboard/__tests__/utils.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/utils.test.tsx similarity index 100% rename from app/src/pages/ProtocolDashboard/__tests__/utils.test.tsx rename to app/src/pages/ODD/ProtocolDashboard/__tests__/utils.test.tsx diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ODD/ProtocolDashboard/index.tsx similarity index 96% rename from app/src/pages/ProtocolDashboard/index.tsx rename to app/src/pages/ODD/ProtocolDashboard/index.tsx index 79118a42860..fa7e7aac10a 100644 --- a/app/src/pages/ProtocolDashboard/index.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/index.tsx @@ -16,22 +16,22 @@ import { } from '@opentrons/components' import { useAllProtocolsQuery } from '@opentrons/react-api-client' -import { SmallButton } from '../../atoms/buttons' -import { Navigation } from '../../organisms/Navigation' +import { SmallButton } from '../../../atoms/buttons' +import { Navigation } from '../../../organisms/Navigation' import { getPinnedProtocolIds, getProtocolsOnDeviceSortKey, updateConfigValue, -} from '../../redux/config' +} from '../../../redux/config' import { PinnedProtocolCarousel } from './PinnedProtocolCarousel' import { sortProtocols } from './utils' import { ProtocolCard } from './ProtocolCard' import { NoProtocols } from './NoProtocols' import { DeleteProtocolConfirmationModal } from './DeleteProtocolConfirmationModal' -import { useNotifyAllRunsQuery } from '../../resources/runs' +import { useNotifyAllRunsQuery } from '../../../resources/runs' -import type { Dispatch } from '../../redux/types' -import type { ProtocolsOnDeviceSortKey } from '../../redux/config/types' +import type { Dispatch } from '../../../redux/types' +import type { ProtocolsOnDeviceSortKey } from '../../../redux/config/types' import type { ProtocolResource } from '@opentrons/shared-data' export function ProtocolDashboard(): JSX.Element { diff --git a/app/src/pages/ProtocolDashboard/utils.ts b/app/src/pages/ODD/ProtocolDashboard/utils.ts similarity index 95% rename from app/src/pages/ProtocolDashboard/utils.ts rename to app/src/pages/ODD/ProtocolDashboard/utils.ts index cdf873b4298..cd8f3b9a2d0 100644 --- a/app/src/pages/ProtocolDashboard/utils.ts +++ b/app/src/pages/ODD/ProtocolDashboard/utils.ts @@ -1,6 +1,6 @@ import type { ProtocolResource } from '@opentrons/shared-data' import type { RunData } from '@opentrons/api-client' -import type { ProtocolsOnDeviceSortKey } from '../../redux/config/types' +import type { ProtocolsOnDeviceSortKey } from '../../../redux/config/types' const DUMMY_FOR_NO_DATE_CASE = -8640000000000000 diff --git a/app/src/pages/ProtocolDetails/Deck.tsx b/app/src/pages/ODD/ProtocolDetails/Deck.tsx similarity index 91% rename from app/src/pages/ProtocolDetails/Deck.tsx rename to app/src/pages/ODD/ProtocolDetails/Deck.tsx index d9c95508264..6dc1a5e95bf 100644 --- a/app/src/pages/ProtocolDetails/Deck.tsx +++ b/app/src/pages/ODD/ProtocolDetails/Deck.tsx @@ -8,9 +8,9 @@ import { } from '@opentrons/react-api-client' import { getLabwareDefURI } from '@opentrons/shared-data' -import { LabwareStackModal } from '../../organisms/Devices/ProtocolRun/SetupLabware/LabwareStackModal' -import { SingleLabwareModal } from '../../organisms/ProtocolSetupLabware/SingleLabwareModal' -import { getLabwareSetupItemGroups } from '../../pages/Protocols/utils' +import { LabwareStackModal } from '../../../organisms/Devices/ProtocolRun/SetupLabware/LabwareStackModal' +import { SingleLabwareModal } from '../../../organisms/ODD/ProtocolSetup/ProtocolSetupLabware/SingleLabwareModal' +import { getLabwareSetupItemGroups } from '../../../transformations/commands' import type { LabwareDefinition2, diff --git a/app/src/pages/ProtocolDetails/EmptySection.tsx b/app/src/pages/ODD/ProtocolDetails/EmptySection.tsx similarity index 100% rename from app/src/pages/ProtocolDetails/EmptySection.tsx rename to app/src/pages/ODD/ProtocolDetails/EmptySection.tsx diff --git a/app/src/pages/ProtocolDetails/Hardware.tsx b/app/src/pages/ODD/ProtocolDetails/Hardware.tsx similarity index 96% rename from app/src/pages/ProtocolDetails/Hardware.tsx rename to app/src/pages/ODD/ProtocolDetails/Hardware.tsx index 11960cb464f..66adc22594c 100644 --- a/app/src/pages/ProtocolDetails/Hardware.tsx +++ b/app/src/pages/ODD/ProtocolDetails/Hardware.tsx @@ -28,12 +28,15 @@ import { import { useGripperDisplayName, usePipetteNameSpecs, -} from '../../resources/instruments/hooks' -import { useRequiredProtocolHardware } from '../Protocols/hooks' +} from '../../../resources/instruments/hooks' +import { useRequiredProtocolHardware } from '../../../pages/Desktop/Protocols/hooks' import { EmptySection } from './EmptySection' import type { TFunction } from 'i18next' -import type { ProtocolHardware, ProtocolPipette } from '../Protocols/hooks' +import type { + ProtocolHardware, + ProtocolPipette, +} from '../../../transformations/commands' const Table = styled('table')` ${TYPOGRAPHY.labelRegular} diff --git a/app/src/pages/ProtocolDetails/Labware.tsx b/app/src/pages/ODD/ProtocolDetails/Labware.tsx similarity index 98% rename from app/src/pages/ProtocolDetails/Labware.tsx rename to app/src/pages/ODD/ProtocolDetails/Labware.tsx index d496b236439..555d67e70a8 100644 --- a/app/src/pages/ProtocolDetails/Labware.tsx +++ b/app/src/pages/ODD/ProtocolDetails/Labware.tsx @@ -15,7 +15,7 @@ import { } from '@opentrons/components' import { getLabwareDisplayName } from '@opentrons/shared-data' -import { useRequiredProtocolLabware } from '../Protocols/hooks' +import { useRequiredProtocolLabware } from '../../../pages/Desktop/Protocols/hooks' import { EmptySection } from './EmptySection' const Table = styled('table')` diff --git a/app/src/pages/ProtocolDetails/Liquids.tsx b/app/src/pages/ODD/ProtocolDetails/Liquids.tsx similarity index 100% rename from app/src/pages/ProtocolDetails/Liquids.tsx rename to app/src/pages/ODD/ProtocolDetails/Liquids.tsx diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ODD/ProtocolDetails/Parameters.tsx similarity index 96% rename from app/src/pages/ProtocolDetails/Parameters.tsx rename to app/src/pages/ODD/ProtocolDetails/Parameters.tsx index 23b99faf884..9d5b692e2ba 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ODD/ProtocolDetails/Parameters.tsx @@ -16,8 +16,8 @@ import { TYPOGRAPHY, WRAP, } from '@opentrons/components' -import { useToaster } from '../../organisms/ToasterOven' -import { useRunTimeParameters } from '../Protocols/hooks' +import { useToaster } from '../../../organisms/ToasterOven' +import { useRunTimeParameters } from '../../../pages/Desktop/Protocols/hooks' import { EmptySection } from './EmptySection' import type { RunTimeParameter } from '@opentrons/shared-data' diff --git a/app/src/pages/ProtocolDetails/__tests__/Deck.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Deck.test.tsx similarity index 97% rename from app/src/pages/ProtocolDetails/__tests__/Deck.test.tsx rename to app/src/pages/ODD/ProtocolDetails/__tests__/Deck.test.tsx index 8b65a5fbef3..01d4efaac48 100644 --- a/app/src/pages/ProtocolDetails/__tests__/Deck.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Deck.test.tsx @@ -3,13 +3,13 @@ import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { useProtocolAnalysisAsDocumentQuery, useProtocolQuery, } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { Deck } from '../Deck' import type { UseQueryResult } from 'react-query' diff --git a/app/src/pages/ProtocolDetails/__tests__/EmptySection.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/EmptySection.test.tsx similarity index 91% rename from app/src/pages/ProtocolDetails/__tests__/EmptySection.test.tsx rename to app/src/pages/ODD/ProtocolDetails/__tests__/EmptySection.test.tsx index 5e340240720..970f45d1514 100644 --- a/app/src/pages/ProtocolDetails/__tests__/EmptySection.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/EmptySection.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import { it, describe } from 'vitest' import { screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { EmptySection } from '../EmptySection' const render = (props: React.ComponentProps) => { diff --git a/app/src/pages/ProtocolDetails/__tests__/Hardware.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx similarity index 91% rename from app/src/pages/ProtocolDetails/__tests__/Hardware.test.tsx rename to app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx index c342086fcb1..969641ab1de 100644 --- a/app/src/pages/ProtocolDetails/__tests__/Hardware.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx @@ -7,13 +7,13 @@ import { WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useRequiredProtocolHardware } from '../../Protocols/hooks' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { useRequiredProtocolHardware } from '../../../../pages/Desktop/Protocols/hooks' import { Hardware } from '../Hardware' -vi.mock('../../Protocols/hooks') -vi.mock('../../../redux/config') +vi.mock('../../../../pages/Desktop/Protocols/hooks') +vi.mock('../../../../redux/config') const MOCK_PROTOCOL_ID = 'mock_protocol_id' diff --git a/app/src/pages/ProtocolDetails/__tests__/Labware.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Labware.test.tsx similarity index 90% rename from app/src/pages/ProtocolDetails/__tests__/Labware.test.tsx rename to app/src/pages/ODD/ProtocolDetails/__tests__/Labware.test.tsx index ccc38662a64..d68aa0bcb56 100644 --- a/app/src/pages/ProtocolDetails/__tests__/Labware.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Labware.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import { vi, it, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useRequiredProtocolLabware } from '../../Protocols/hooks' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { useRequiredProtocolLabware } from '../../../../pages/Desktop/Protocols/hooks' import { Labware } from '../Labware' import { @@ -15,7 +15,7 @@ import { import type { LabwareDefinition2 } from '@opentrons/shared-data' import { screen } from '@testing-library/react' -vi.mock('../../Protocols/hooks') +vi.mock('../../../../pages/Desktop/Protocols/hooks') const MOCK_PROTOCOL_ID = 'mock_protocol_id' diff --git a/app/src/pages/ProtocolDetails/__tests__/Liquids.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Liquids.test.tsx similarity index 98% rename from app/src/pages/ProtocolDetails/__tests__/Liquids.test.tsx rename to app/src/pages/ODD/ProtocolDetails/__tests__/Liquids.test.tsx index b60bbe34a84..e75efad6803 100644 --- a/app/src/pages/ProtocolDetails/__tests__/Liquids.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Liquids.test.tsx @@ -12,8 +12,8 @@ import { parseLiquidsInLoadOrder, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { Liquids } from '../Liquids' import type { UseQueryResult } from 'react-query' diff --git a/app/src/pages/ProtocolDetails/__tests__/Parameters.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Parameters.test.tsx similarity index 78% rename from app/src/pages/ProtocolDetails/__tests__/Parameters.test.tsx rename to app/src/pages/ODD/ProtocolDetails/__tests__/Parameters.test.tsx index 0f7099e3416..26460dd6904 100644 --- a/app/src/pages/ProtocolDetails/__tests__/Parameters.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Parameters.test.tsx @@ -2,15 +2,15 @@ import * as React from 'react' import { when } from 'vitest-when' import { it, describe, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' -import { i18n } from '../../../i18n' -import { useToaster } from '../../../organisms/ToasterOven' -import { renderWithProviders } from '../../../__testing-utils__' -import { useRunTimeParameters } from '../../Protocols/hooks' +import { i18n } from '../../../../i18n' +import { useToaster } from '../../../../organisms/ToasterOven' +import { renderWithProviders } from '../../../../__testing-utils__' +import { useRunTimeParameters } from '../../../../pages/Desktop/Protocols/hooks' import { Parameters } from '../Parameters' import { mockRunTimeParameterData } from '../fixtures' -vi.mock('../../../organisms/ToasterOven') -vi.mock('../../Protocols/hooks') +vi.mock('../../../../organisms/ToasterOven') +vi.mock('../../../../pages/Desktop/Protocols/hooks') const render = (props: React.ComponentProps) => { return renderWithProviders(, { diff --git a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/ProtocolDetails.test.tsx similarity index 87% rename from app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx rename to app/src/pages/ODD/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index 9cef0e28bc0..9f48408c6be 100644 --- a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -4,7 +4,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { when } from 'vitest-when' import { Route, MemoryRouter, Routes } from 'react-router-dom' import '@testing-library/jest-dom/vitest' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { deleteProtocol, deleteRun, getProtocol } from '@opentrons/api-client' import { useCreateRunMutation, @@ -12,15 +12,13 @@ import { useProtocolQuery, useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' -import { useHardwareStatusText } from '../../../organisms/OnDeviceDisplay/RobotDashboard/hooks' -import { useOffsetCandidatesForAnalysis } from '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' -import { - useMissingProtocolHardware, - useRunTimeParameters, -} from '../../Protocols/hooks' -import { ProtocolSetupParameters } from '../../../organisms/ProtocolSetupParameters' -import { formatTimeWithUtcLabel } from '../../../resources/runs' +import { i18n } from '../../../../i18n' +import { useHardwareStatusText } from '../../../../organisms/ODD/RobotDashboard/hooks' +import { useOffsetCandidatesForAnalysis } from '../../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useRunTimeParameters } from '../../../../pages/Desktop/Protocols/hooks' +import { ProtocolSetupParameters } from '../../../../organisms/ODD/ProtocolSetup/ProtocolSetupParameters' +import { formatTimeWithUtcLabel } from '../../../../resources/runs' +import { useMissingProtocolHardware } from '../../../../transformations/commands' import { ProtocolDetails } from '..' import { Deck } from '../Deck' import { Hardware } from '../Hardware' @@ -42,19 +40,22 @@ Object.defineProperty(window, 'IntersectionObserver', { configurable: true, value: IntersectionObserver, }) -vi.mock('../../../organisms/ProtocolSetupParameters') +vi.mock( + '../../../../organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters' +) vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') -vi.mock('../../../organisms/OnDeviceDisplay/RobotDashboard/hooks') +vi.mock('../../../../organisms/ODD/RobotDashboard/hooks') vi.mock( - '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' + '../../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' ) -vi.mock('../../Protocols/hooks') +vi.mock('../../../../pages/Desktop/Protocols/hooks') +vi.mock('../../../../transformations/commands') vi.mock('../Deck') vi.mock('../Hardware') vi.mock('../Labware') vi.mock('../Parameters') -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') const MOCK_HOST_CONFIG = {} as HostConfig const mockCreateRun = vi.fn((id: string) => {}) diff --git a/app/src/pages/ProtocolDetails/fixtures.ts b/app/src/pages/ODD/ProtocolDetails/fixtures.ts similarity index 100% rename from app/src/pages/ProtocolDetails/fixtures.ts rename to app/src/pages/ODD/ProtocolDetails/fixtures.ts diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ODD/ProtocolDetails/index.tsx similarity index 93% rename from app/src/pages/ProtocolDetails/index.tsx rename to app/src/pages/ODD/ProtocolDetails/index.tsx index 73f9a5bd590..931baae070a 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ODD/ProtocolDetails/index.tsx @@ -31,38 +31,36 @@ import { useProtocolAnalysisAsDocumentQuery, useProtocolQuery, } from '@opentrons/react-api-client' -import { MAXIMUM_PINNED_PROTOCOLS } from '../../App/constants' -import { MediumButton, SmallButton } from '../../atoms/buttons' +import { MAXIMUM_PINNED_PROTOCOLS } from '../../../App/constants' +import { MediumButton, SmallButton } from '../../../atoms/buttons' import { ProtocolDetailsHeaderChipSkeleton, ProcotolDetailsHeaderTitleSkeleton, ProtocolDetailsSectionContentSkeleton, -} from '../../organisms/OnDeviceDisplay/ProtocolDetails' -import { useHardwareStatusText } from '../../organisms/OnDeviceDisplay/RobotDashboard/hooks' -import { OddModal, SmallModalChildren } from '../../molecules/OddModal' -import { useToaster } from '../../organisms/ToasterOven' +} from '../../../organisms/ODD/ProtocolDetails' +import { useHardwareStatusText } from '../../../organisms/ODD/RobotDashboard/hooks' +import { OddModal, SmallModalChildren } from '../../../molecules/OddModal' +import { useToaster } from '../../../organisms/ToasterOven' import { getApplyHistoricOffsets, getPinnedProtocolIds, updateConfigValue, -} from '../../redux/config' -import { useOffsetCandidatesForAnalysis } from '../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' -import { - useMissingProtocolHardware, - useRunTimeParameters, -} from '../Protocols/hooks' -import { ProtocolSetupParameters } from '../../organisms/ProtocolSetupParameters' +} from '../../../redux/config' +import { useOffsetCandidatesForAnalysis } from '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useRunTimeParameters } from '../../../pages/Desktop/Protocols/hooks' +import { useMissingProtocolHardware } from '../../../transformations/commands' +import { ProtocolSetupParameters } from '../../../organisms/ODD/ProtocolSetup/ProtocolSetupParameters' import { Parameters } from './Parameters' import { Deck } from './Deck' import { Hardware } from './Hardware' import { Labware } from './Labware' import { Liquids } from './Liquids' -import { formatTimeWithUtcLabel } from '../../resources/runs' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import type { Protocol } from '@opentrons/api-client' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' -import type { Dispatch } from '../../redux/types' -import type { OnDeviceRouteParams } from '../../App/types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' +import type { Dispatch } from '../../../redux/types' +import type { OnDeviceRouteParams } from '../../../App/types' interface ProtocolHeaderProps { title?: string | null @@ -521,7 +519,7 @@ export function ProtocolDetails(): JSX.Element | null { isScrolled={isScrolled} isProtocolFetching={isProtocolFetching} /> - + void diff --git a/app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx b/app/src/pages/ODD/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx similarity index 89% rename from app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx rename to app/src/pages/ODD/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx index 25c22e5e734..c837d91f518 100644 --- a/app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx +++ b/app/src/pages/ODD/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx @@ -8,10 +8,10 @@ import { LegacyStyledText, } from '@opentrons/components' -import { SmallButton } from '../../atoms/buttons' -import { OddModal } from '../../molecules/OddModal' +import { SmallButton } from '../../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' interface ConfirmSetupStepsCompleteModalProps { onCloseClick: () => void diff --git a/app/src/pages/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx b/app/src/pages/ODD/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx similarity index 88% rename from app/src/pages/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx rename to app/src/pages/ODD/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx index e28d6102f85..7354352b53a 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx +++ b/app/src/pages/ODD/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx @@ -2,10 +2,10 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { ConfirmAttachedModal } from '../../../pages/ProtocolSetup/ConfirmAttachedModal' +import { i18n } from '../../../../i18n' +import { ConfirmAttachedModal } from '../ConfirmAttachedModal' const mockOnCloseClick = vi.fn() const mockOnConfirmClick = vi.fn() diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx similarity index 84% rename from app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx rename to app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 04f3c2d8e77..2622c88e726 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -13,8 +13,8 @@ import { useModulesQuery, useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { mockHeaterShaker } from '../../../redux/modules/__fixtures__' +import { renderWithProviders } from '../../../../__testing-utils__' +import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' import { getDeckDefFromRobotType, FLEX_ROBOT_TYPE, @@ -22,9 +22,9 @@ import { flexDeckDefV5, } from '@opentrons/shared-data' -import { i18n } from '../../../i18n' -import { useToaster } from '../../../organisms/ToasterOven' -import { mockRobotSideAnalysis } from '../../../molecules/Command/__fixtures__' +import { i18n } from '../../../../i18n' +import { useToaster } from '../../../../organisms/ToasterOven' +import { mockRobotSideAnalysis } from '../../../../molecules/Command/__fixtures__' import { useAttachedModules, useLPCDisabledReason, @@ -33,34 +33,38 @@ import { useRobotType, useRunCreatedAtTimestamp, useTrackProtocolRunEvent, -} from '../../../organisms/Devices/hooks' -import { getLocalRobot } from '../../../redux/discovery' -import { ANALYTICS_PROTOCOL_RUN_ACTION } from '../../../redux/analytics' -import { ProtocolSetupLiquids } from '../../../organisms/ProtocolSetupLiquids' -import { getProtocolModulesInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' -import { ProtocolSetupModulesAndDeck } from '../../../organisms/ProtocolSetupModulesAndDeck' -import { ProtocolSetupLabware } from '../../../organisms/ProtocolSetupLabware' -import { ProtocolSetupOffsets } from '../../../organisms/ProtocolSetupOffsets' -import { getUnmatchedModulesForProtocol } from '../../../organisms/ProtocolSetupModulesAndDeck/utils' -import { useLaunchLPC } from '../../../organisms/LabwarePositionCheck/useLaunchLPC' -import { ConfirmCancelRunModal } from '../../../organisms/OnDeviceDisplay/RunningProtocol' -import { mockProtocolModuleInfo } from '../../../organisms/ProtocolSetupInstruments/__fixtures__' -import { getIncompleteInstrumentCount } from '../../../organisms/ProtocolSetupInstruments/utils' +} from '../../../../organisms/Devices/hooks' +import { getLocalRobot } from '../../../../redux/discovery' +import { ANALYTICS_PROTOCOL_RUN_ACTION } from '../../../../redux/analytics' +import { getProtocolModulesInfo } from '../../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' +import { + ProtocolSetupLabware, + ProtocolSetupLiquids, + ProtocolSetupModulesAndDeck, + ProtocolSetupOffsets, + ViewOnlyParameters, + ProtocolSetupTitleSkeleton, + ProtocolSetupStepSkeleton, + getUnmatchedModulesForProtocol, + getIncompleteInstrumentCount, +} from '../../../../organisms/ODD/ProtocolSetup' +import { useLaunchLPC } from '../../../../organisms/LabwarePositionCheck/useLaunchLPC' +import { ConfirmCancelRunModal } from '../../../../organisms/ODD/RunningProtocol' +import { mockProtocolModuleInfo } from '../../../../organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/__fixtures__' import { useProtocolHasRunTimeParameters, useRunControls, useRunStatus, -} from '../../../organisms/RunTimeControl/hooks' -import { useIsHeaterShakerInProtocol } from '../../../organisms/ModuleCard/hooks' -import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' -import { ConfirmAttachedModal } from '../../../pages/ProtocolSetup/ConfirmAttachedModal' -import { ConfirmSetupStepsCompleteModal } from '../../../pages/ProtocolSetup/ConfirmSetupStepsCompleteModal' -import { ProtocolSetup } from '../../../pages/ProtocolSetup' -import { useNotifyRunQuery } from '../../../resources/runs' -import { ViewOnlyParameters } from '../../../organisms/ProtocolSetupParameters/ViewOnlyParameters' -import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' -import { mockRunTimeParameterData } from '../../ProtocolDetails/fixtures' -import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' +} from '../../../../organisms/RunTimeControl/hooks' +import { useIsHeaterShakerInProtocol } from '../../../../organisms/ModuleCard/hooks' +import { useNotifyDeckConfigurationQuery } from '../../../../resources/deck_configuration/useNotifyDeckConfigurationQuery' +import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' +import { ConfirmAttachedModal } from '../ConfirmAttachedModal' +import { ConfirmSetupStepsCompleteModal } from '../ConfirmSetupStepsCompleteModal' +import { ProtocolSetup } from '../' +import { useNotifyRunQuery } from '../../../../resources/runs' +import { mockConnectableRobot } from '../../../../redux/discovery/__fixtures__' +import { mockRunTimeParameterData } from '../../../../pages/ODD/ProtocolDetails/fixtures' import type { UseQueryResult } from 'react-query' import type * as SharedData from '@opentrons/shared-data' @@ -97,28 +101,34 @@ vi.mock('react-router-dom', async importOriginal => { }) vi.mock('@opentrons/react-api-client') -vi.mock('../../../organisms/LabwarePositionCheck/useLaunchLPC') -vi.mock('../../../organisms/Devices/hooks') -vi.mock('../../../organisms/ProtocolSetupParameters/ViewOnlyParameters') +vi.mock('../../../../organisms/LabwarePositionCheck/useLaunchLPC') +vi.mock('../../../../organisms/Devices/hooks') +vi.mock('../../../../organisms/ODD/ProtocolSetup', async importOriginal => { + const ACTUALS = ['ProtocolSetupStep'] + const actual = await importOriginal() + return Object.fromEntries( + Object.entries(actual).map(([k, v]) => + ACTUALS.includes(k) ? [k, v] : [k, vi.fn()] + ) + ) +}) vi.mock( - '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' + '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) -vi.mock('../../../organisms/ProtocolSetupInstruments/utils') -vi.mock('../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo') -vi.mock('../../../organisms/ProtocolSetupModulesAndDeck') -vi.mock('../../../organisms/ProtocolSetupModulesAndDeck/utils') -vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol') -vi.mock('../../../organisms/RunTimeControl/hooks') -vi.mock('../../../organisms/ProtocolSetupLiquids') -vi.mock('../../../organisms/ProtocolSetupLabware') -vi.mock('../../../organisms/ProtocolSetupOffsets') -vi.mock('../../../organisms/ModuleCard/hooks') -vi.mock('../../../redux/discovery/selectors') +vi.mock( + '../../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' +) +vi.mock('../../../../organisms/ODD/RunningProtocol') +vi.mock('../../../../organisms/RunTimeControl/hooks') +vi.mock('../../../../organisms/ModuleCard/hooks') +vi.mock('../../../../redux/discovery/selectors') vi.mock('../ConfirmAttachedModal') -vi.mock('../../../organisms/ToasterOven') -vi.mock('../../../resources/deck_configuration/hooks') -vi.mock('../../../resources/runs') -vi.mock('../../../resources/deck_configuration') +vi.mock('../../../../organisms/ToasterOven') +vi.mock('../../../../resources/runs') +vi.mock('../../../../resources/deck_configuration/hooks') +vi.mock( + '../../../../resources/deck_configuration/useNotifyDeckConfigurationQuery' +) vi.mock('../ConfirmSetupStepsCompleteModal') const render = (path = '/') => { @@ -137,6 +147,8 @@ const render = (path = '/') => { const MockProtocolSetupLabware = vi.mocked(ProtocolSetupLabware) const MockProtocolSetupLiquids = vi.mocked(ProtocolSetupLiquids) const MockProtocolSetupOffsets = vi.mocked(ProtocolSetupOffsets) +const MockProtocolSetupTitleSkeleton = vi.mocked(ProtocolSetupTitleSkeleton) +const MockProtocolSetupStepSkeleton = vi.mocked(ProtocolSetupStepSkeleton) const MockConfirmSetupStepsCompleteModal = vi.mocked( ConfirmSetupStepsCompleteModal ) @@ -257,6 +269,7 @@ describe('ProtocolSetup', () => { isStopRunActionLoading: false, isResetRunLoading: false, isResumeRunFromRecoveryActionLoading: false, + isRunControlLoading: false, }) when(vi.mocked(useRunStatus)).calledWith(RUN_ID).thenReturn(RUN_STATUS_IDLE) vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({ @@ -536,8 +549,10 @@ describe('ProtocolSetup', () => { vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({ data: null, } as any) + MockProtocolSetupTitleSkeleton.mockReturnValue(
SKELETON
) + MockProtocolSetupStepSkeleton.mockReturnValue(
SKELETON
) render(`/runs/${RUN_ID}/setup/`) - expect(screen.getAllByTestId('Skeleton').length).toBeGreaterThan(0) + expect(screen.getAllByText('SKELETON').length).toBeGreaterThanOrEqual(2) }) it('should render toast and make a button disabled when a robot door is open', () => { diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ODD/ProtocolSetup/index.tsx similarity index 77% rename from app/src/pages/ProtocolSetup/index.tsx rename to app/src/pages/ODD/ProtocolSetup/index.tsx index 5a50c52c19c..c0c0e0fe83d 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ODD/ProtocolSetup/index.tsx @@ -4,24 +4,19 @@ import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { useNavigate, useParams } from 'react-router-dom' import first from 'lodash/first' -import { css } from 'styled-components' import { RUN_STATUS_IDLE, RUN_STATUS_STOPPED } from '@opentrons/api-client' import { ALIGN_CENTER, BORDERS, - Btn, COLORS, DIRECTION_COLUMN, Flex, - Icon, - JUSTIFY_END, JUSTIFY_SPACE_BETWEEN, + LegacyStyledText, OVERFLOW_WRAP_ANYWHERE, POSITION_STICKY, SPACING, - LegacyStyledText, - TEXT_ALIGN_RIGHT, truncateString, TYPOGRAPHY, useConditionalConfirm, @@ -36,13 +31,8 @@ import { getDeckDefFromRobotType, getModuleDisplayName, getFixtureDisplayName, - SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' -import { - ProtocolSetupTitleSkeleton, - ProtocolSetupStepSkeleton, -} from '../../organisms/OnDeviceDisplay/ProtocolSetup' import { useAttachedModules, useLPCDisabledReason, @@ -51,226 +41,65 @@ import { useRobotAnalyticsData, useRobotType, useTrackProtocolRunEvent, -} from '../../organisms/Devices/hooks' -import { - useRequiredProtocolHardwareFromAnalysis, - useMissingProtocolHardwareFromAnalysis, -} from '../Protocols/hooks' -import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' -import { ProtocolSetupLabware } from '../../organisms/ProtocolSetupLabware' -import { ProtocolSetupModulesAndDeck } from '../../organisms/ProtocolSetupModulesAndDeck' -import { ProtocolSetupLiquids } from '../../organisms/ProtocolSetupLiquids' -import { ProtocolSetupOffsets } from '../../organisms/ProtocolSetupOffsets' -import { ProtocolSetupInstruments } from '../../organisms/ProtocolSetupInstruments' -import { ProtocolSetupDeckConfiguration } from '../../organisms/ProtocolSetupDeckConfiguration' -import { useLaunchLPC } from '../../organisms/LabwarePositionCheck/useLaunchLPC' -import { getUnmatchedModulesForProtocol } from '../../organisms/ProtocolSetupModulesAndDeck/utils' -import { ConfirmCancelRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol' -import { AnalysisFailedModal } from '../../organisms/ProtocolSetupParameters/AnalysisFailedModal' +} from '../../../organisms/Devices/hooks' + +import { getProtocolModulesInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { + AnalysisFailedModal, + ProtocolSetupDeckConfiguration, + ProtocolSetupInstruments, + ProtocolSetupLabware, + ProtocolSetupLiquids, + ProtocolSetupModulesAndDeck, + ProtocolSetupOffsets, + ProtocolSetupStep, + ProtocolSetupStepSkeleton, + ProtocolSetupTitleSkeleton, + getUnmatchedModulesForProtocol, getIncompleteInstrumentCount, - getProtocolUsesGripper, -} from '../../organisms/ProtocolSetupInstruments/utils' + ViewOnlyParameters, +} from '../../../organisms/ODD/ProtocolSetup' +import { useLaunchLPC } from '../../../organisms/LabwarePositionCheck/useLaunchLPC' +import { ConfirmCancelRunModal } from '../../../organisms/ODD/RunningProtocol' import { useRunControls, useRunStatus, -} from '../../organisms/RunTimeControl/hooks' -import { useToaster } from '../../organisms/ToasterOven' -import { useIsHeaterShakerInProtocol } from '../../organisms/ModuleCard/hooks' -import { getLabwareSetupItemGroups } from '../Protocols/utils' -import { getLocalRobot, getRobotSerialNumber } from '../../redux/discovery' +} from '../../../organisms/RunTimeControl/hooks' +import { useToaster } from '../../../organisms/ToasterOven' +import { useIsHeaterShakerInProtocol } from '../../../organisms/ModuleCard/hooks' +import { getLocalRobot, getRobotSerialNumber } from '../../../redux/discovery' import { ANALYTICS_PROTOCOL_PROCEED_TO_RUN, ANALYTICS_PROTOCOL_RUN_ACTION, useTrackEvent, -} from '../../redux/analytics' -import { getIsHeaterShakerAttached } from '../../redux/config' +} from '../../../redux/analytics' +import { getIsHeaterShakerAttached } from '../../../redux/config' import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { ConfirmSetupStepsCompleteModal } from './ConfirmSetupStepsCompleteModal' -import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' +import { getLatestCurrentOffsets } from '../../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { CloseButton, PlayButton } from './Buttons' -import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' -import { getRequiredDeckConfig } from '../../resources/deck_configuration/utils' -import { useNotifyRunQuery } from '../../resources/runs' -import { ViewOnlyParameters } from '../../organisms/ProtocolSetupParameters/ViewOnlyParameters' +import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' +import { getRequiredDeckConfig } from '../../../resources/deck_configuration/utils' +import { useNotifyRunQuery } from '../../../resources/runs' import type { Run } from '@opentrons/api-client' import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' -import type { OnDeviceRouteParams } from '../../App/types' +import type { OnDeviceRouteParams } from '../../../App/types' +import type { ProtocolModuleInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' +import type { SetupScreens } from '../../../organisms/ODD/ProtocolSetup' import type { ProtocolHardware, ProtocolFixture, -} from '../../pages/Protocols/hooks' -import type { ProtocolModuleInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' +} from '../../../transformations/commands' +import { + getLabwareSetupItemGroups, + getProtocolUsesGripper, + useRequiredProtocolHardwareFromAnalysis, + useMissingProtocolHardwareFromAnalysis, +} from '../../../transformations/commands' const FETCH_DURATION_MS = 5000 -export type ProtocolSetupStepStatus = - | 'ready' - | 'not ready' - | 'general' - | 'inform' -interface ProtocolSetupStepProps { - onClickSetupStep: () => void - status: ProtocolSetupStepStatus - title: string - // first line of detail text - detail?: string | null - // second line of detail text - subDetail?: string | null - // disallow click handler, disabled styling - disabled?: boolean - // disallow click handler, don't show CTA icons, allow styling - interactionDisabled?: boolean - // display the reason the setup step is disabled - disabledReason?: string | null - // optional description - description?: string | null - // optional removal of the left icon - hasLeftIcon?: boolean - // optional removal of the right icon - hasRightIcon?: boolean - // optional enlarge the font size - fontSize?: string -} - -export function ProtocolSetupStep({ - onClickSetupStep, - status, - title, - detail, - subDetail, - disabled = false, - interactionDisabled = false, - disabledReason, - description, - hasRightIcon = true, - hasLeftIcon = true, - fontSize = 'p', -}: ProtocolSetupStepProps): JSX.Element { - const isInteractionDisabled = interactionDisabled || disabled - const backgroundColorByStepStatus = { - ready: COLORS.green35, - 'not ready': COLORS.yellow35, - general: COLORS.grey35, - inform: COLORS.grey35, - } - const { makeSnackbar } = useToaster() - - const makeDisabledReasonSnackbar = (): void => { - if (disabledReason != null) { - makeSnackbar(disabledReason) - } - } - - let backgroundColor: string - if (!disabled) { - switch (status) { - case 'general': - backgroundColor = COLORS.blue35 - break - case 'ready': - backgroundColor = COLORS.green40 - break - case 'inform': - backgroundColor = COLORS.grey50 - break - default: - backgroundColor = COLORS.yellow40 - } - } else backgroundColor = '' - - const PUSHED_STATE_STYLE = css` - &:active { - background-color: ${backgroundColor}; - } - ` - - const isToggle = detail === 'On' || detail === 'Off' - - return ( - { - !isInteractionDisabled - ? onClickSetupStep() - : makeDisabledReasonSnackbar() - }} - width="100%" - data-testid={`SetupButton_${title}`} - > - - {status !== 'general' && - !disabled && - status !== 'inform' && - hasLeftIcon ? ( - - ) : null} - - - {title} - - {description != null ? ( - - {description} - - ) : null} - - - - {detail} - {subDetail != null && detail != null ?
: null} - {subDetail} -
-
- {interactionDisabled || !hasRightIcon ? null : ( - - )} -
-
- ) -} - const ANALYSIS_POLL_MS = 5000 interface PrepareToRunProps { runId: string @@ -449,11 +278,7 @@ function PrepareToRun({ const isCurrentFixtureCompatible = cutoutFixtureId != null && compatibleCutoutFixtureIds.includes(cutoutFixtureId) - return ( - !isCurrentFixtureCompatible && - cutoutFixtureId != null && - !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) - ) + return !isCurrentFixtureCompatible && cutoutFixtureId != null } ) const isLocationConflict = locationConflictSlots.some( @@ -515,6 +340,7 @@ function PrepareToRun({ : 'not ready' // Liquids information const liquidsInProtocol = mostRecentAnalysis?.liquids ?? [] + const areLiquidsInProtocol = liquidsInProtocol.length > 0 const isReadyToRun = incompleteInstrumentCount === 0 && areModulesReady && areFixturesReady @@ -528,7 +354,7 @@ function PrepareToRun({ !( labwareConfirmed && offsetsConfirmed && - (liquidsConfirmed || liquidsInProtocol.length === 0) + (liquidsConfirmed || !areLiquidsInProtocol) ) ) { confirmStepsComplete() @@ -803,18 +629,16 @@ function PrepareToRun({ }} title={i18n.format(t('liquids'), 'capitalize')} status={ - liquidsConfirmed || liquidsInProtocol.length === 0 - ? 'ready' - : 'general' + liquidsConfirmed || !areLiquidsInProtocol ? 'ready' : 'general' } detail={ - liquidsInProtocol.length > 0 + areLiquidsInProtocol ? t('initial_liquids_num', { count: liquidsInProtocol.length, }) : t('liquids_not_in_setup') } - interactionDisabled={liquidsInProtocol.length === 0} + interactionDisabled={!areLiquidsInProtocol} /> ) : ( @@ -836,16 +660,6 @@ function PrepareToRun({ ) } -export type SetupScreens = - | 'prepare to run' - | 'instruments' - | 'modules' - | 'offsets' - | 'labware' - | 'liquids' - | 'deck configuration' - | 'view only parameters' - export function ProtocolSetup(): JSX.Element { const { runId } = useParams< keyof OnDeviceRouteParams @@ -889,6 +703,8 @@ export function ProtocolSetup(): JSX.Element { } ) + const areLiquidsInProtocol = (mostRecentAnalysis?.liquids?.length ?? 0) > 0 + React.useEffect(() => { if (mostRecentAnalysis?.status === 'completed') { setIsPollingForCompletedAnalysis(false) @@ -954,7 +770,7 @@ export function ProtocolSetup(): JSX.Element { const missingSteps = [ !offsetsConfirmed ? t('applied_labware_offsets') : null, !labwareConfirmed ? t('labware_placement') : null, - !liquidsConfirmed ? t('liquids') : null, + !liquidsConfirmed && areLiquidsInProtocol ? t('liquids') : null, ].filter(s => s != null) const { confirm: confirmMissingSteps, @@ -1044,6 +860,7 @@ export function ProtocolSetup(): JSX.Element { error.detail)} /> ) : null} diff --git a/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx b/app/src/pages/ODD/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx similarity index 75% rename from app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx rename to app/src/pages/ODD/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx index 8b81936b7b6..c612c1a6c82 100644 --- a/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx @@ -1,29 +1,25 @@ import * as React from 'react' import { useNavigate } from 'react-router-dom' import { useQueryClient } from 'react-query' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' +import { Trans, useTranslation } from 'react-i18next' import { deleteProtocol, deleteRun, getProtocol } from '@opentrons/api-client' import { ALIGN_CENTER, - Box, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, Flex, - OVERFLOW_HIDDEN, - OVERFLOW_WRAP_ANYWHERE, SPACING, - TYPOGRAPHY, + StyledText, } from '@opentrons/components' import { useHost, useProtocolQuery } from '@opentrons/react-api-client' -import { SmallButton } from '../../atoms/buttons' -import { OddModal } from '../../molecules/OddModal' -import { useToaster } from '../../organisms/ToasterOven' +import { SmallButton } from '../../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' +import { useToaster } from '../../../organisms/ToasterOven' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' interface DeleteTransferConfirmationModalProps { transferId: string @@ -97,10 +93,17 @@ export function DeleteTransferConfirmationModal({ gridGap={SPACING.spacing32} width="100%" > - - {transferName} - {t('will_be_deleted')} - + + + + + ) } - -const TransferNameText = styled.span` - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - overflow: ${OVERFLOW_HIDDEN}; - overflow-wrap: ${OVERFLOW_WRAP_ANYWHERE}; - font-weight: ${TYPOGRAPHY.fontWeightBold}; - font-size: ${TYPOGRAPHY.fontSize22}; - line-height: ${TYPOGRAPHY.lineHeight28}; - color: ${COLORS.grey60}; -` -const AdditionalText = styled.span` - font-weight: ${TYPOGRAPHY.fontWeightRegular}; - font-size: ${TYPOGRAPHY.fontSize22}; - line-height: ${TYPOGRAPHY.lineHeight28}; - color: ${COLORS.grey60}; -` diff --git a/app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx b/app/src/pages/ODD/QuickTransferDashboard/IntroductoryModal.tsx similarity index 86% rename from app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx rename to app/src/pages/ODD/QuickTransferDashboard/IntroductoryModal.tsx index 4d42324958a..0d469669070 100644 --- a/app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/IntroductoryModal.tsx @@ -8,10 +8,10 @@ import { ALIGN_CENTER, TEXT_ALIGN_CENTER, } from '@opentrons/components' -import { OddModal } from '../../molecules/OddModal' -import { SmallButton } from '../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' +import { SmallButton } from '../../../atoms/buttons' -import imgSrc from '../../assets/images/on-device-display/odd-abstract-6.png' +import imgSrc from '../../../assets/images/on-device-display/odd-abstract-6.png' interface IntroductoryModalProps { onClose: () => void diff --git a/app/src/pages/QuickTransferDashboard/LongPressModal.tsx b/app/src/pages/ODD/QuickTransferDashboard/LongPressModal.tsx similarity index 93% rename from app/src/pages/QuickTransferDashboard/LongPressModal.tsx rename to app/src/pages/ODD/QuickTransferDashboard/LongPressModal.tsx index ac45a2cf386..90e082ff276 100644 --- a/app/src/pages/QuickTransferDashboard/LongPressModal.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/LongPressModal.tsx @@ -12,16 +12,16 @@ import { } from '@opentrons/components' import { useCreateRunMutation } from '@opentrons/react-api-client' -import { MAXIMUM_PINNED_PROTOCOLS } from '../../App/constants' -import { SmallModalChildren } from '../../molecules/OddModal' -import { useToaster } from '../../organisms/ToasterOven' +import { MAXIMUM_PINNED_PROTOCOLS } from '../../../App/constants' +import { SmallModalChildren } from '../../../molecules/OddModal' +import { useToaster } from '../../../organisms/ToasterOven' import { getPinnedQuickTransferIds, updateConfigValue, -} from '../../redux/config' +} from '../../../redux/config' import type { UseLongPressResult } from '@opentrons/components' -import type { Dispatch } from '../../redux/types' +import type { Dispatch } from '../../../redux/types' interface LongPressModalProps { longpress: UseLongPressResult diff --git a/app/src/pages/QuickTransferDashboard/NoQuickTransfers.tsx b/app/src/pages/ODD/QuickTransferDashboard/NoQuickTransfers.tsx similarity index 91% rename from app/src/pages/QuickTransferDashboard/NoQuickTransfers.tsx rename to app/src/pages/ODD/QuickTransferDashboard/NoQuickTransfers.tsx index e81bb829b35..66aae9b45c4 100644 --- a/app/src/pages/QuickTransferDashboard/NoQuickTransfers.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/NoQuickTransfers.tsx @@ -12,7 +12,7 @@ import { StyledText, } from '@opentrons/components' -import imgSrc from '../../assets/images/on-device-display/empty_quick_transfer_dashboard.png' +import imgSrc from '../../../assets/images/on-device-display/empty_quick_transfer_dashboard.png' export function NoQuickTransfers(): JSX.Element { const { t } = useTranslation('quick_transfer') diff --git a/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx b/app/src/pages/ODD/QuickTransferDashboard/PinnedTransfer.tsx similarity index 98% rename from app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx rename to app/src/pages/ODD/QuickTransferDashboard/PinnedTransfer.tsx index cafcaa299a5..f196bba7ecd 100644 --- a/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/PinnedTransfer.tsx @@ -18,7 +18,7 @@ import { } from '@opentrons/components' import { LongPressModal } from './LongPressModal' -import { formatTimeWithUtcLabel } from '../../resources/runs' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' diff --git a/app/src/pages/QuickTransferDashboard/PinnedTransferCarousel.tsx b/app/src/pages/ODD/QuickTransferDashboard/PinnedTransferCarousel.tsx similarity index 100% rename from app/src/pages/QuickTransferDashboard/PinnedTransferCarousel.tsx rename to app/src/pages/ODD/QuickTransferDashboard/PinnedTransferCarousel.tsx diff --git a/app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx b/app/src/pages/ODD/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx similarity index 92% rename from app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx rename to app/src/pages/ODD/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx index ac1142364c4..1649db05846 100644 --- a/app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx @@ -8,8 +8,8 @@ import { DIRECTION_COLUMN, TYPOGRAPHY, } from '@opentrons/components' -import { OddModal } from '../../molecules/OddModal' -import { SmallButton } from '../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' +import { SmallButton } from '../../../atoms/buttons' interface PipetteNotAttachedErrorModalProps { onExit: () => void diff --git a/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx b/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx similarity index 97% rename from app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx rename to app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx index 9044d228cb0..23d593d0933 100644 --- a/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx @@ -30,14 +30,14 @@ import { } from '@opentrons/react-api-client' import { deleteProtocol } from '@opentrons/api-client' -import { SmallButton } from '../../atoms/buttons' -import { OddModal } from '../../molecules/OddModal' +import { SmallButton } from '../../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' import { LongPressModal } from './LongPressModal' -import { formatTimeWithUtcLabel } from '../../resources/runs' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' -import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types' +import type { OddModalHeaderBaseProps } from '../../../molecules/OddModal/types' const REFETCH_INTERVAL = 5000 diff --git a/app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx b/app/src/pages/ODD/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx similarity index 91% rename from app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx rename to app/src/pages/ODD/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx index ed663b57b73..41eb4b92ce8 100644 --- a/app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx @@ -8,8 +8,8 @@ import { DIRECTION_COLUMN, TYPOGRAPHY, } from '@opentrons/components' -import { OddModal } from '../../molecules/OddModal' -import { SmallButton } from '../../atoms/buttons' +import { OddModal } from '../../../molecules/OddModal' +import { SmallButton } from '../../../atoms/buttons' interface StorageLimitReachedErrorModalProps { onExit: () => void diff --git a/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx b/app/src/pages/ODD/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx similarity index 93% rename from app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx rename to app/src/pages/ODD/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx index 177eb93a691..0dee122f94f 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx @@ -4,11 +4,11 @@ import { when } from 'vitest-when' import { act, fireEvent, screen } from '@testing-library/react' import { getProtocol, deleteProtocol, deleteRun } from '@opentrons/api-client' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { useHost, useProtocolQuery } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' -import { useToaster } from '../../../organisms/ToasterOven' +import { i18n } from '../../../../i18n' +import { useToaster } from '../../../../organisms/ToasterOven' import { DeleteTransferConfirmationModal } from '../DeleteTransferConfirmationModal' import type { NavigateFunction } from 'react-router-dom' @@ -18,7 +18,7 @@ const mockNavigate = vi.fn() vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') -vi.mock('../../../organisms/ToasterOven') +vi.mock('../../../../organisms/ToasterOven') vi.mock('react-router-dom', async importOriginal => { const reactRouterDom = await importOriginal() return { diff --git a/app/src/pages/QuickTransferDashboard/__tests__/LongPressModal.test.tsx b/app/src/pages/ODD/QuickTransferDashboard/__tests__/LongPressModal.test.tsx similarity index 95% rename from app/src/pages/QuickTransferDashboard/__tests__/LongPressModal.test.tsx rename to app/src/pages/ODD/QuickTransferDashboard/__tests__/LongPressModal.test.tsx index 01b2f410c50..b2324325755 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/LongPressModal.test.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/__tests__/LongPressModal.test.tsx @@ -7,8 +7,8 @@ import { fireEvent, renderHook, screen } from '@testing-library/react' import { useLongPress } from '@opentrons/components' import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { LongPressModal } from '../LongPressModal' import type { HostConfig } from '@opentrons/api-client' diff --git a/app/src/pages/QuickTransferDashboard/__tests__/NoQuickTransfers.test.tsx b/app/src/pages/ODD/QuickTransferDashboard/__tests__/NoQuickTransfers.test.tsx similarity index 87% rename from app/src/pages/QuickTransferDashboard/__tests__/NoQuickTransfers.test.tsx rename to app/src/pages/ODD/QuickTransferDashboard/__tests__/NoQuickTransfers.test.tsx index 22b3337a0d5..4ce4718e418 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/NoQuickTransfers.test.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/__tests__/NoQuickTransfers.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { describe, it, expect } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { NoQuickTransfers } from '../NoQuickTransfers' import { screen } from '@testing-library/react' diff --git a/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx b/app/src/pages/ODD/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx similarity index 95% rename from app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx rename to app/src/pages/ODD/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx index 28588dbccb1..2700ac9f133 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx @@ -3,8 +3,8 @@ import { vi, it, describe, expect } from 'vitest' import { act, fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { PinnedTransfer } from '../PinnedTransfer' import type { ProtocolResource } from '@opentrons/shared-data' diff --git a/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx b/app/src/pages/ODD/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx similarity index 98% rename from app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx rename to app/src/pages/ODD/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx index 6853233b08f..ad6e392e085 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx @@ -7,8 +7,8 @@ import { useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { QuickTransferCard } from '../QuickTransferCard' import { LongPressModal } from '../LongPressModal' import type { NavigateFunction } from 'react-router-dom' diff --git a/app/src/pages/QuickTransferDashboard/index.tsx b/app/src/pages/ODD/QuickTransferDashboard/index.tsx similarity index 97% rename from app/src/pages/QuickTransferDashboard/index.tsx rename to app/src/pages/ODD/QuickTransferDashboard/index.tsx index b47d14f5ed8..40fbc87889f 100644 --- a/app/src/pages/QuickTransferDashboard/index.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/index.tsx @@ -21,14 +21,14 @@ import { useInstrumentsQuery, } from '@opentrons/react-api-client' -import { SmallButton, FloatingActionButton } from '../../atoms/buttons' -import { Navigation } from '../../organisms/Navigation' +import { SmallButton, FloatingActionButton } from '../../../atoms/buttons' +import { Navigation } from '../../../organisms/Navigation' import { getPinnedQuickTransferIds, getQuickTransfersOnDeviceSortKey, getHasDismissedQuickTransferIntro, updateConfigValue, -} from '../../redux/config' +} from '../../../redux/config' import { PinnedTransferCarousel } from './PinnedTransferCarousel' import { sortQuickTransfers } from './utils' import { QuickTransferCard } from './QuickTransferCard' @@ -40,8 +40,8 @@ import { DeleteTransferConfirmationModal } from './DeleteTransferConfirmationMod import type { ProtocolResource } from '@opentrons/shared-data' import type { PipetteData } from '@opentrons/api-client' -import type { Dispatch } from '../../redux/types' -import type { QuickTransfersOnDeviceSortKey } from '../../redux/config/types' +import type { Dispatch } from '../../../redux/types' +import type { QuickTransfersOnDeviceSortKey } from '../../../redux/config/types' export function QuickTransferDashboard(): JSX.Element { const protocols = useAllProtocolsQuery() diff --git a/app/src/pages/QuickTransferDashboard/utils.ts b/app/src/pages/ODD/QuickTransferDashboard/utils.ts similarity index 91% rename from app/src/pages/QuickTransferDashboard/utils.ts rename to app/src/pages/ODD/QuickTransferDashboard/utils.ts index a64f94476e9..706a5e4679b 100644 --- a/app/src/pages/QuickTransferDashboard/utils.ts +++ b/app/src/pages/ODD/QuickTransferDashboard/utils.ts @@ -1,5 +1,5 @@ import type { ProtocolResource } from '@opentrons/shared-data' -import type { ProtocolsOnDeviceSortKey } from '../../redux/config/types' +import type { ProtocolsOnDeviceSortKey } from '../../../redux/config/types' export function sortQuickTransfers( sortBy: ProtocolsOnDeviceSortKey, diff --git a/app/src/pages/QuickTransferDetails/Deck.tsx b/app/src/pages/ODD/QuickTransferDetails/Deck.tsx similarity index 100% rename from app/src/pages/QuickTransferDetails/Deck.tsx rename to app/src/pages/ODD/QuickTransferDetails/Deck.tsx diff --git a/app/src/pages/QuickTransferDetails/Hardware.tsx b/app/src/pages/ODD/QuickTransferDetails/Hardware.tsx similarity index 96% rename from app/src/pages/QuickTransferDetails/Hardware.tsx rename to app/src/pages/ODD/QuickTransferDetails/Hardware.tsx index 399f9764f97..f8765ead16d 100644 --- a/app/src/pages/QuickTransferDetails/Hardware.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/Hardware.tsx @@ -26,10 +26,13 @@ import { import { useGripperDisplayName, usePipetteNameSpecs, -} from '../../resources/instruments/hooks' -import { useRequiredProtocolHardware } from '../Protocols/hooks' +} from '../../../resources/instruments/hooks' +import { useRequiredProtocolHardware } from '../../../pages/Desktop/Protocols/hooks' -import type { ProtocolHardware, ProtocolPipette } from '../Protocols/hooks' +import type { + ProtocolHardware, + ProtocolPipette, +} from '../../../transformations/commands' import type { TFunction } from 'i18next' const Table = styled('table')` diff --git a/app/src/pages/QuickTransferDetails/Labware.tsx b/app/src/pages/ODD/QuickTransferDetails/Labware.tsx similarity index 97% rename from app/src/pages/QuickTransferDetails/Labware.tsx rename to app/src/pages/ODD/QuickTransferDetails/Labware.tsx index 718fbaf1c80..09ad78714bd 100644 --- a/app/src/pages/QuickTransferDetails/Labware.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/Labware.tsx @@ -15,7 +15,7 @@ import { } from '@opentrons/components' import { getLabwareDisplayName } from '@opentrons/shared-data' -import { useRequiredProtocolLabware } from '../Protocols/hooks' +import { useRequiredProtocolLabware } from '../../../pages/Desktop/Protocols/hooks' const Table = styled('table')` ${TYPOGRAPHY.labelRegular} diff --git a/app/src/pages/QuickTransferDetails/__tests__/Deck.test.tsx b/app/src/pages/ODD/QuickTransferDetails/__tests__/Deck.test.tsx similarity index 97% rename from app/src/pages/QuickTransferDetails/__tests__/Deck.test.tsx rename to app/src/pages/ODD/QuickTransferDetails/__tests__/Deck.test.tsx index b9826b58fab..bd0b68c68f2 100644 --- a/app/src/pages/QuickTransferDetails/__tests__/Deck.test.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/__tests__/Deck.test.tsx @@ -3,13 +3,13 @@ import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { useProtocolAnalysisAsDocumentQuery, useProtocolQuery, } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { Deck } from '../Deck' import type { UseQueryResult } from 'react-query' diff --git a/app/src/pages/QuickTransferDetails/__tests__/Hardware.test.tsx b/app/src/pages/ODD/QuickTransferDetails/__tests__/Hardware.test.tsx similarity index 89% rename from app/src/pages/QuickTransferDetails/__tests__/Hardware.test.tsx rename to app/src/pages/ODD/QuickTransferDetails/__tests__/Hardware.test.tsx index 451d14a2001..460f00178e7 100644 --- a/app/src/pages/QuickTransferDetails/__tests__/Hardware.test.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/__tests__/Hardware.test.tsx @@ -7,13 +7,14 @@ import { WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useRequiredProtocolHardware } from '../../Protocols/hooks' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { useRequiredProtocolHardware } from '../../../../pages/Desktop/Protocols/hooks' import { Hardware } from '../Hardware' -vi.mock('../../Protocols/hooks') -vi.mock('../../../redux/config') +vi.mock('../../../../transformations/commands') +vi.mock('../../../../pages/Desktop/Protocols/hooks') +vi.mock('../../../../redux/config') const MOCK_PROTOCOL_ID = 'mock_protocol_id' diff --git a/app/src/pages/QuickTransferDetails/__tests__/Labware.test.tsx b/app/src/pages/ODD/QuickTransferDetails/__tests__/Labware.test.tsx similarity index 90% rename from app/src/pages/QuickTransferDetails/__tests__/Labware.test.tsx rename to app/src/pages/ODD/QuickTransferDetails/__tests__/Labware.test.tsx index 87171e90513..a9eed293a6a 100644 --- a/app/src/pages/QuickTransferDetails/__tests__/Labware.test.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/__tests__/Labware.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import { vi, it, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { useRequiredProtocolLabware } from '../../Protocols/hooks' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { useRequiredProtocolLabware } from '../../../../pages/Desktop/Protocols/hooks' import { Labware } from '../Labware' import { @@ -15,7 +15,7 @@ import { import type { LabwareDefinition2 } from '@opentrons/shared-data' import { screen } from '@testing-library/react' -vi.mock('../../Protocols/hooks') +vi.mock('../../../../pages/Desktop/Protocols/hooks') const MOCK_PROTOCOL_ID = 'mock_protocol_id' diff --git a/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx b/app/src/pages/ODD/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx similarity index 88% rename from app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx rename to app/src/pages/ODD/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx index 929f5d46f82..9a5f16dbd02 100644 --- a/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx @@ -4,18 +4,18 @@ import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import { Route, MemoryRouter, Routes } from 'react-router-dom' import '@testing-library/jest-dom/vitest' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' import { useCreateRunMutation, useHost, useProtocolQuery, useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' -import { i18n } from '../../../i18n' -import { useHardwareStatusText } from '../../../organisms/OnDeviceDisplay/RobotDashboard/hooks' -import { useOffsetCandidatesForAnalysis } from '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' -import { useMissingProtocolHardware } from '../../Protocols/hooks' -import { formatTimeWithUtcLabel } from '../../../resources/runs' +import { i18n } from '../../../../i18n' +import { useHardwareStatusText } from '../../../../organisms/ODD/RobotDashboard/hooks' +import { useOffsetCandidatesForAnalysis } from '../../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useMissingProtocolHardware } from '../../../../transformations/commands' +import { formatTimeWithUtcLabel } from '../../../../resources/runs' import { DeleteTransferConfirmationModal } from '../../QuickTransferDashboard/DeleteTransferConfirmationModal' import { QuickTransferDetails } from '..' import { Deck } from '../Deck' @@ -36,19 +36,19 @@ Object.defineProperty(window, 'IntersectionObserver', { configurable: true, value: IntersectionObserver, }) -vi.mock('../../../organisms/ProtocolSetupParameters') +vi.mock('../../../../organisms/ODD/ProtocolSetup/ProtocolSetupParameters') vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') -vi.mock('../../../organisms/OnDeviceDisplay/RobotDashboard/hooks') +vi.mock('../../../../organisms/ODD/RobotDashboard/hooks') vi.mock( - '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' + '../../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' ) vi.mock('../../QuickTransferDashboard/DeleteTransferConfirmationModal') -vi.mock('../../Protocols/hooks') +vi.mock('../../../../transformations/commands') vi.mock('../Deck') vi.mock('../Hardware') vi.mock('../Labware') -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') const MOCK_HOST_CONFIG = {} as HostConfig const mockCreateRun = vi.fn((id: string) => {}) diff --git a/app/src/pages/QuickTransferDetails/index.tsx b/app/src/pages/ODD/QuickTransferDetails/index.tsx similarity index 92% rename from app/src/pages/QuickTransferDetails/index.tsx rename to app/src/pages/ODD/QuickTransferDetails/index.tsx index 1423ff109aa..e0b92bd1f6c 100644 --- a/app/src/pages/QuickTransferDetails/index.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/index.tsx @@ -30,32 +30,32 @@ import { useProtocolAnalysisAsDocumentQuery, useProtocolQuery, } from '@opentrons/react-api-client' -import { MAXIMUM_PINNED_PROTOCOLS } from '../../App/constants' -import { MediumButton, SmallButton } from '../../atoms/buttons' +import { MAXIMUM_PINNED_PROTOCOLS } from '../../../App/constants' +import { MediumButton, SmallButton } from '../../../atoms/buttons' import { ProtocolDetailsHeaderChipSkeleton, ProcotolDetailsHeaderTitleSkeleton, ProtocolDetailsSectionContentSkeleton, -} from '../../organisms/OnDeviceDisplay/ProtocolDetails' -import { useHardwareStatusText } from '../../organisms/OnDeviceDisplay/RobotDashboard/hooks' -import { SmallModalChildren } from '../../molecules/OddModal' -import { useToaster } from '../../organisms/ToasterOven' +} from '../../../organisms/ODD/ProtocolDetails' +import { useHardwareStatusText } from '../../../organisms/ODD/RobotDashboard/hooks' +import { SmallModalChildren } from '../../../molecules/OddModal' +import { useToaster } from '../../../organisms/ToasterOven' import { getApplyHistoricOffsets, getPinnedQuickTransferIds, updateConfigValue, -} from '../../redux/config' -import { useOffsetCandidatesForAnalysis } from '../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' -import { useMissingProtocolHardware } from '../Protocols/hooks' +} from '../../../redux/config' +import { useOffsetCandidatesForAnalysis } from '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useMissingProtocolHardware } from '../../../transformations/commands' import { DeleteTransferConfirmationModal } from '../QuickTransferDashboard/DeleteTransferConfirmationModal' import { Deck } from './Deck' import { Hardware } from './Hardware' import { Labware } from './Labware' -import { formatTimeWithUtcLabel } from '../../resources/runs' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import type { Protocol } from '@opentrons/api-client' -import type { Dispatch } from '../../redux/types' -import type { OnDeviceRouteParams } from '../../App/types' +import type { Dispatch } from '../../../redux/types' +import type { OnDeviceRouteParams } from '../../../App/types' interface QuickTransferHeaderProps { title?: string | null @@ -354,7 +354,7 @@ export function QuickTransferDetails(): JSX.Element | null { makeSnackbar(t('unpinned_transfer') as string) } dispatch( - updateConfigValue('protocols.pinnedTransferIds', pinnedTransferIds) + updateConfigValue('protocols.pinnedQuickTransferIds', pinnedTransferIds) ) } const handleRunTransfer = (): void => { @@ -403,7 +403,7 @@ export function QuickTransferDetails(): JSX.Element | null { isScrolled={isScrolled} isTransferFetching={isTransferFetching} /> - + { } }) vi.mock('@opentrons/react-api-client') -vi.mock('../../../organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun') -vi.mock( - '../../../organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCarousel' -) -vi.mock('../../../organisms/Navigation') -vi.mock('../../Protocols/hooks') -vi.mock('../../../redux/config') +vi.mock('../../../../organisms/ODD/RobotDashboard/EmptyRecentRun') +vi.mock('../../../../organisms/ODD/RobotDashboard/RecentRunProtocolCarousel') +vi.mock('../../../../organisms/Navigation') +vi.mock('../../../../transformations/commands') +vi.mock('../../../../redux/config') vi.mock('../WelcomeModal') -vi.mock('../../../resources/runs') +vi.mock('../../../../resources/runs') const render = () => { return renderWithProviders( diff --git a/app/src/pages/RobotDashboard/__tests__/WelcomeModal.test.tsx b/app/src/pages/ODD/RobotDashboard/__tests__/WelcomeModal.test.tsx similarity index 93% rename from app/src/pages/RobotDashboard/__tests__/WelcomeModal.test.tsx rename to app/src/pages/ODD/RobotDashboard/__tests__/WelcomeModal.test.tsx index ec3fc2fc5ff..c0b73b0bb8c 100644 --- a/app/src/pages/RobotDashboard/__tests__/WelcomeModal.test.tsx +++ b/app/src/pages/ODD/RobotDashboard/__tests__/WelcomeModal.test.tsx @@ -4,13 +4,13 @@ import { fireEvent, screen } from '@testing-library/react' import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' import { WelcomeModal } from '../WelcomeModal' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' -vi.mock('../../../redux/config') +vi.mock('../../../../redux/config') vi.mock('@opentrons/react-api-client') const mockFunc = vi.fn() diff --git a/app/src/pages/RobotDashboard/index.tsx b/app/src/pages/ODD/RobotDashboard/index.tsx similarity index 79% rename from app/src/pages/RobotDashboard/index.tsx rename to app/src/pages/ODD/RobotDashboard/index.tsx index d3e2084e948..609e57bca65 100644 --- a/app/src/pages/RobotDashboard/index.tsx +++ b/app/src/pages/ODD/RobotDashboard/index.tsx @@ -10,16 +10,17 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' +import { useAllProtocolsQuery } from '@opentrons/react-api-client' -import { Navigation } from '../../organisms/Navigation' +import { Navigation } from '../../../organisms/Navigation' import { EmptyRecentRun, RecentRunProtocolCarousel, -} from '../../organisms/OnDeviceDisplay/RobotDashboard' -import { getOnDeviceDisplaySettings } from '../../redux/config' +} from '../../../organisms/ODD/RobotDashboard' +import { getOnDeviceDisplaySettings } from '../../../redux/config' import { WelcomeModal } from './WelcomeModal' -import { ServerInitializing } from '../../organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing' -import { useNotifyAllRunsQuery } from '../../resources/runs' +import { ServerInitializing } from '../../../organisms/ODD/RobotDashboard/ServerInitializing' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import type { RunData } from '@opentrons/api-client' export const MAXIMUM_RECENT_RUN_PROTOCOLS = 8 @@ -30,6 +31,7 @@ export function RobotDashboard(): JSX.Element { data: allRunsQueryData, error: allRunsQueryError, } = useNotifyAllRunsQuery() + const protocols = useAllProtocolsQuery() const { unfinishedUnboxingFlowRoute } = useSelector( getOnDeviceDisplaySettings @@ -45,6 +47,11 @@ export function RobotDashboard(): JSX.Element { acc.some(collectedRun => collectedRun.protocolId === run.protocolId) ) { return acc + } else if ( + protocols?.data?.data.find(protocol => protocol.id === run.protocolId) + ?.protocolKind === 'quick-transfer' + ) { + return acc } else { return [...acc, run] } diff --git a/app/src/pages/RobotSettingsDashboard/RobotSettingsList.tsx b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx similarity index 85% rename from app/src/pages/RobotSettingsDashboard/RobotSettingsList.tsx rename to app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx index 8fe6e3a5719..6ea3cc37e6e 100644 --- a/app/src/pages/RobotSettingsDashboard/RobotSettingsList.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx @@ -21,8 +21,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { getLocalRobot, getRobotApiVersion } from '../../redux/discovery' -import { getRobotUpdateAvailable } from '../../redux/robot-update' +import { getLocalRobot, getRobotApiVersion } from '../../../redux/discovery' +import { getRobotUpdateAvailable } from '../../../redux/robot-update' import { DEV_INTERNAL_FLAGS, getApplyHistoricOffsets, @@ -31,17 +31,20 @@ import { toggleDevInternalFlag, toggleDevtools, toggleHistoricOffsets, -} from '../../redux/config' -import { InlineNotification } from '../../atoms/InlineNotification' -import { getRobotSettings, updateSetting } from '../../redux/robot-settings' -import { UNREACHABLE } from '../../redux/discovery/constants' -import { Navigation } from '../../organisms/Navigation' -import { useLEDLights } from '../../organisms/Devices/hooks' -import { useNetworkConnection } from '../../resources/networking/hooks/useNetworkConnection' -import { RobotSettingButton } from '../../pages/RobotSettingsDashboard/RobotSettingButton' +} from '../../../redux/config' +import { InlineNotification } from '../../../atoms/InlineNotification' +import { getRobotSettings, updateSetting } from '../../../redux/robot-settings' +import { UNREACHABLE } from '../../../redux/discovery/constants' +import { Navigation } from '../../../organisms/Navigation' +import { useLEDLights } from '../../../organisms/Devices/hooks' +import { useNetworkConnection } from '../../../resources/networking/hooks/useNetworkConnection' +import { + RobotSettingButton, + OnOffToggle, +} from '../../../organisms/ODD/RobotSettingsDashboard' -import type { Dispatch, State } from '../../redux/types' -import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' +import type { Dispatch, State } from '../../../redux/types' +import type { SetSettingOption } from '../../../organisms/ODD/RobotSettingsDashboard' const HOME_GANTRY_SETTING_ID = 'disableHomeOnBoot' interface RobotSettingsListProps { @@ -264,21 +267,3 @@ function FeatureFlags(): JSX.Element { ) } - -export function OnOffToggle(props: { isOn: boolean }): JSX.Element { - const { t } = useTranslation('shared') - return ( - - - {props.isOn ? t('on') : t('off')} - - - ) -} diff --git a/app/src/pages/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx b/app/src/pages/ODD/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx similarity index 78% rename from app/src/pages/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx rename to app/src/pages/ODD/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx index 9d73a1a77bc..78ba8bbd0b5 100644 --- a/app/src/pages/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/__tests__/RobotSettingsDashboard.test.tsx @@ -3,14 +3,14 @@ import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' - -import { i18n } from '../../../i18n' -import { getRobotSettings } from '../../../redux/robot-settings' -import { getLocalRobot } from '../../../redux/discovery' -import { toggleDevtools, toggleHistoricOffsets } from '../../../redux/config' -import { mockConnectedRobot } from '../../../redux/discovery/__fixtures__' -import { Navigation } from '../../../organisms/Navigation' +import { renderWithProviders } from '../../../../__testing-utils__' + +import { i18n } from '../../../../i18n' +import { getRobotSettings } from '../../../../redux/robot-settings' +import { getLocalRobot } from '../../../../redux/discovery' +import { toggleDevtools, toggleHistoricOffsets } from '../../../../redux/config' +import { mockConnectedRobot } from '../../../../redux/discovery/__fixtures__' +import { Navigation } from '../../../../organisms/Navigation' import { DeviceReset, TouchScreenSleep, @@ -19,27 +19,29 @@ import { Privacy, RobotSystemVersion, UpdateChannel, -} from '../../../organisms/RobotSettingsDashboard' -import { getRobotUpdateAvailable } from '../../../redux/robot-update' -import { useNetworkConnection } from '../../../resources/networking/hooks/useNetworkConnection' -import { useLEDLights } from '../../../organisms/Devices/hooks' - -import { RobotSettingsDashboard } from '../../../pages/RobotSettingsDashboard' - -vi.mock('../../../redux/discovery') -vi.mock('../../../redux/robot-update') -vi.mock('../../../redux/config') -vi.mock('../../../redux/robot-settings') -vi.mock('../../../resources/networking/hooks/useNetworkConnection') -vi.mock('../../../organisms/Navigation') -vi.mock('../../../organisms/RobotSettingsDashboard/TouchScreenSleep') -vi.mock('../../../organisms/RobotSettingsDashboard/NetworkSettings') -vi.mock('../../../organisms/RobotSettingsDashboard/DeviceReset') -vi.mock('../../../organisms/RobotSettingsDashboard/RobotSystemVersion') -vi.mock('../../../organisms/RobotSettingsDashboard/TouchscreenBrightness') -vi.mock('../../../organisms/RobotSettingsDashboard/UpdateChannel') -vi.mock('../../../organisms/Devices/hooks') -vi.mock('../../../organisms/RobotSettingsDashboard/Privacy') +} from '../../../../organisms/ODD/RobotSettingsDashboard' +import { getRobotUpdateAvailable } from '../../../../redux/robot-update' +import { useNetworkConnection } from '../../../../resources/networking/hooks/useNetworkConnection' +import { useLEDLights } from '../../../../organisms/Devices/hooks' + +import { RobotSettingsDashboard } from '../' + +vi.mock('../../../../redux/discovery') +vi.mock('../../../../redux/robot-update') +vi.mock('../../../../redux/config') +vi.mock('../../../../redux/robot-settings') +vi.mock('../../../../resources/networking/hooks/useNetworkConnection') +vi.mock('../../../../organisms/Navigation') +vi.mock('../../../../organisms/ODD/RobotSettingsDashboard/TouchScreenSleep') +vi.mock('../../../../organisms/ODD/RobotSettingsDashboard/NetworkSettings') +vi.mock('../../../../organisms/ODD/RobotSettingsDashboard/DeviceReset') +vi.mock('../../../../organisms/ODD/RobotSettingsDashboard/RobotSystemVersion') +vi.mock( + '../../../../organisms/ODD/RobotSettingsDashboard/TouchscreenBrightness' +) +vi.mock('../../../../organisms/ODD/RobotSettingsDashboard/UpdateChannel') +vi.mock('../../../../organisms/Devices/hooks') +vi.mock('../../../../organisms/ODD/RobotSettingsDashboard/Privacy') const mockToggleLights = vi.fn() diff --git a/app/src/pages/RobotSettingsDashboard/index.tsx b/app/src/pages/ODD/RobotSettingsDashboard/index.tsx similarity index 82% rename from app/src/pages/RobotSettingsDashboard/index.tsx rename to app/src/pages/ODD/RobotSettingsDashboard/index.tsx index 8de5bca79df..e144111b579 100644 --- a/app/src/pages/RobotSettingsDashboard/index.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/index.tsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import last from 'lodash/last' -import { EthernetConnectionDetails } from '../../organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails' +import { EthernetConnectionDetails } from '../../../organisms/ODD/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails' import { DeviceReset, TouchscreenBrightness, @@ -18,46 +18,25 @@ import { RobotSettingsWifiConnect, RobotSystemVersion, UpdateChannel, -} from '../../organisms/RobotSettingsDashboard' +} from '../../../organisms/ODD/RobotSettingsDashboard' import { getRobotUpdateAvailable, getRobotUpdateInfoForRobot, -} from '../../redux/robot-update' +} from '../../../redux/robot-update' import { getLocalRobot, getRobotApiVersion, UNREACHABLE, -} from '../../redux/discovery' -import { fetchStatus, postWifiConfigure } from '../../redux/networking' -import { getRequestById, useDispatchApiRequest } from '../../redux/robot-api' -import { useWifiList } from '../../resources/networking/hooks' -import { useNetworkConnection } from '../../resources/networking/hooks/useNetworkConnection' -import { RobotSettingsList } from '../../pages/RobotSettingsDashboard/RobotSettingsList' +} from '../../../redux/discovery' +import { fetchStatus, postWifiConfigure } from '../../../redux/networking' +import { getRequestById, useDispatchApiRequest } from '../../../redux/robot-api' +import { useWifiList } from '../../../resources/networking/hooks' +import { useNetworkConnection } from '../../../resources/networking/hooks/useNetworkConnection' +import { RobotSettingsList } from './RobotSettingsList' import type { WifiSecurityType } from '@opentrons/api-client' -import type { Dispatch, State } from '../../redux/types' - -/** - * a set of screen options for the robot settings dashboard page - */ -export type SettingOption = - | 'NetworkSettings' - | 'RobotName' - | 'RobotSystemVersion' - | 'TouchscreenSleep' - | 'TouchscreenBrightness' - | 'TextSize' - | 'Privacy' - | 'DeviceReset' - | 'UpdateChannel' - | 'EthernetConnectionDetails' - | 'RobotSettingsSelectAuthenticationType' - | 'RobotSettingsJoinOtherNetwork' - | 'RobotSettingsSetWifiCred' - | 'RobotSettingsWifi' - | 'RobotSettingsWifiConnect' - -export type SetSettingOption = (option: SettingOption | null) => void +import type { Dispatch, State } from '../../../redux/types' +import type { SettingOption } from '../../../organisms/ODD/RobotSettingsDashboard' export function RobotSettingsDashboard(): JSX.Element { const { i18n, t } = useTranslation('shared') diff --git a/app/src/pages/RunSummary/index.tsx b/app/src/pages/ODD/RunSummary/index.tsx similarity index 89% rename from app/src/pages/RunSummary/index.tsx rename to app/src/pages/ODD/RunSummary/index.tsx index a70bc1db7d4..800d14ec9c1 100644 --- a/app/src/pages/RunSummary/index.tsx +++ b/app/src/pages/ODD/RunSummary/index.tsx @@ -40,38 +40,41 @@ import { useDeleteRunMutation, useRunCommandErrors, } from '@opentrons/react-api-client' -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { useRunTimestamps, useRunControls, -} from '../../organisms/RunTimeControl/hooks' +} from '../../../organisms/RunTimeControl/hooks' import { useRunCreatedAtTimestamp, useTrackProtocolRunEvent, useRobotAnalyticsData, -} from '../../organisms/Devices/hooks' -import { useCloseCurrentRun } from '../../organisms/ProtocolUpload/hooks' -import { onDeviceDisplayFormatTimestamp } from '../../organisms/Devices/utils' -import { EMPTY_TIMESTAMP } from '../../organisms/Devices/constants' -import { RunTimer } from '../../organisms/Devices/ProtocolRun/RunTimer' +} from '../../../organisms/Devices/hooks' +import { useCloseCurrentRun } from '../../../organisms/ProtocolUpload/hooks' +import { onDeviceDisplayFormatTimestamp } from '../../../organisms/Devices/utils' +import { EMPTY_TIMESTAMP } from '../../../organisms/Devices/constants' +import { RunTimer } from '../../../organisms/Devices/ProtocolRun/RunTimer' import { useTrackEvent, ANALYTICS_PROTOCOL_RUN_ACTION, ANALYTICS_PROTOCOL_PROCEED_TO_RUN, -} from '../../redux/analytics' -import { getLocalRobot } from '../../redux/discovery' -import { RunFailedModal } from '../../organisms/OnDeviceDisplay/RunningProtocol' -import { formatTimeWithUtcLabel, useNotifyRunQuery } from '../../resources/runs' -import { handleTipsAttachedModal } from '../../organisms/DropTipWizardFlows/TipsAttachedModal' -import { useTipAttachmentStatus } from '../../organisms/DropTipWizardFlows' -import { useRecoveryAnalytics } from '../../organisms/ErrorRecoveryFlows/hooks' +} from '../../../redux/analytics' +import { getLocalRobot } from '../../../redux/discovery' +import { RunFailedModal } from '../../../organisms/ODD/RunningProtocol' +import { + formatTimeWithUtcLabel, + useIsRunCurrent, + useNotifyRunQuery, +} from '../../../resources/runs' +import { + useTipAttachmentStatus, + handleTipsAttachedModal, +} from '../../../organisms/DropTipWizardFlows' +import { useRecoveryAnalytics } from '../../../organisms/ErrorRecoveryFlows/hooks' import type { IconName } from '@opentrons/components' -import type { OnDeviceRouteParams } from '../../App/types' -import type { PipetteWithTip } from '../../organisms/DropTipWizardFlows' - -const CURRENT_RUN_POLL_MS = 5000 +import type { OnDeviceRouteParams } from '../../../App/types' +import type { PipetteWithTip } from '../../../organisms/DropTipWizardFlows' export function RunSummary(): JSX.Element { const { runId } = useParams< @@ -80,11 +83,14 @@ export function RunSummary(): JSX.Element { const { t } = useTranslation('run_details') const navigate = useNavigate() const host = useHost() - const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) - const isRunCurrent = Boolean( - useNotifyRunQuery(runId, { refetchInterval: CURRENT_RUN_POLL_MS })?.data - ?.data?.current - ) + const { data: runRecord } = useNotifyRunQuery(runId, { + staleTime: Infinity, + onError: () => { + // in case the run is remotely deleted by a desktop app, navigate to the dash + navigate('/dashboard') + }, + }) + const isRunCurrent = useIsRunCurrent(runId) const { deleteRun } = useDeleteRunMutation() const runStatus = runRecord?.data.status ?? null const didRunSucceed = runStatus === RUN_STATUS_SUCCEEDED @@ -138,6 +144,19 @@ export function RunSummary(): JSX.Element { const { reset, isResetRunLoading } = useRunControls(runId, onCloneRunSuccess) const trackEvent = useTrackEvent() const { closeCurrentRun, isClosingCurrentRun } = useCloseCurrentRun() + // Close the current run only if it's active and then execute the onSuccess callback. Prefer this wrapper over + // closeCurrentRun directly, since the callback is swallowed if currentRun is null. + const closeCurrentRunIfValid = (onSuccess?: () => void): void => { + if (isRunCurrent) { + closeCurrentRun({ + onSuccess: () => { + onSuccess?.() + }, + }) + } else { + onSuccess?.() + } + } const [showRunFailedModal, setShowRunFailedModal] = React.useState( false ) @@ -165,10 +184,12 @@ export function RunSummary(): JSX.Element { } ) // TODO(jh, 08-14-24): The backend never returns the "user cancelled a run" error and cancelledWithoutRecovery becomes unnecessary. + const cancelledWithoutRecovery = + !enteredER && runStatus === RUN_STATUS_STOPPED const hasCommandErrors = commandErrorList != null && commandErrorList.data.length > 0 const disableErrorDetailsBtn = !( - hasCommandErrors || + (hasCommandErrors && !cancelledWithoutRecovery) || (runRecord?.data.errors != null && runRecord?.data.errors.length > 0) ) @@ -229,16 +250,10 @@ export function RunSummary(): JSX.Element { }, [isRunCurrent, enteredER]) const returnToQuickTransfer = (): void => { - if (!isRunCurrent) { + closeCurrentRunIfValid(() => { deleteRun(runId) - } else { - closeCurrentRun({ - onSuccess: () => { - deleteRun(runId) - }, - }) - } - navigate('/quick-transfer') + navigate('/quick-transfer') + }) } // TODO(jh, 05-30-24): EXEC-487. Refactor reset() so we can redirect to the setup page, showing the shimmer skeleton instead. @@ -269,25 +284,17 @@ export function RunSummary(): JSX.Element { setTipStatusResolved: setTipStatusResolvedAndRoute(handleReturnToDash), host, aPipetteWithTip, - instrumentModelSpecs: aPipetteWithTip.specs, - mount: aPipetteWithTip.mount, - robotType: FLEX_ROBOT_TYPE, - isRunCurrent, - onSkipAndHome: () => { - closeCurrentRun({ - onSettled: () => { - navigate('/') - }, + onSettled: () => { + closeCurrentRunIfValid(() => { + navigate('/dashboard') }) }, }) } else if (isQuickTransfer) { returnToQuickTransfer() } else { - closeCurrentRun({ - onSettled: () => { - navigate('/') - }, + closeCurrentRunIfValid(() => { + navigate('/dashboard') }) } } @@ -298,11 +305,7 @@ export function RunSummary(): JSX.Element { setTipStatusResolved: setTipStatusResolvedAndRoute(handleRunAgain), host, aPipetteWithTip, - instrumentModelSpecs: aPipetteWithTip.specs, - mount: aPipetteWithTip.mount, - robotType: FLEX_ROBOT_TYPE, - isRunCurrent, - onSkipAndHome: () => { + onSettled: () => { runAgain() }, }) diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx similarity index 79% rename from app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx rename to app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx index bddb00263d4..2757da982f2 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -17,58 +17,58 @@ import { useRunActionMutations, } from '@opentrons/react-api-client' -import { renderWithProviders } from '../../../__testing-utils__' -import { mockRobotSideAnalysis } from '../../../molecules/Command/__fixtures__' +import { renderWithProviders } from '../../../../__testing-utils__' +import { mockRobotSideAnalysis } from '../../../../molecules/Command/__fixtures__' import { CurrentRunningProtocolCommand, RunningProtocolSkeleton, -} from '../../../organisms/OnDeviceDisplay/RunningProtocol' -import { mockUseAllCommandsResponseNonDeterministic } from '../../../organisms/RunProgressMeter/__fixtures__' +} from '../../../../organisms/ODD/RunningProtocol' +import { mockUseAllCommandsResponseNonDeterministic } from '../../../../organisms/RunProgressMeter/__fixtures__' import { useRunStatus, useRunTimestamps, -} from '../../../organisms/RunTimeControl/hooks' -import { getLocalRobot } from '../../../redux/discovery' -import { CancelingRunModal } from '../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal' -import { useTrackProtocolRunEvent } from '../../../organisms/Devices/hooks' -import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { OpenDoorAlertModal } from '../../../organisms/OpenDoorAlertModal' +} from '../../../../organisms/RunTimeControl/hooks' +import { getLocalRobot } from '../../../../redux/discovery' +import { CancelingRunModal } from '../../../../organisms/ODD/RunningProtocol/CancelingRunModal' +import { useTrackProtocolRunEvent } from '../../../../organisms/Devices/hooks' +import { useMostRecentCompletedAnalysis } from '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { OpenDoorAlertModal } from '../../../../organisms/OpenDoorAlertModal' import { RunningProtocol } from '..' import { useNotifyRunQuery, useNotifyAllCommandsQuery, -} from '../../../resources/runs' -import { useFeatureFlag } from '../../../redux/config' +} from '../../../../resources/runs' +import { useFeatureFlag } from '../../../../redux/config' import { ErrorRecoveryFlows, useErrorRecoveryFlows, -} from '../../../organisms/ErrorRecoveryFlows' -import { useLastRunCommand } from '../../../organisms/Devices/hooks/useLastRunCommand' +} from '../../../../organisms/ErrorRecoveryFlows' +import { useLastRunCommand } from '../../../../organisms/Devices/hooks/useLastRunCommand' import { useInterventionModal, InterventionModal, -} from '../../../organisms/InterventionModal' +} from '../../../../organisms/InterventionModal' import type { UseQueryResult } from 'react-query' import type { ProtocolAnalyses, RunCommandSummary } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') -vi.mock('../../../organisms/Devices/hooks') -vi.mock('../../../organisms/Devices/hooks/useLastRunCommandKey') -vi.mock('../../../organisms/RunTimeControl/hooks') +vi.mock('../../../../organisms/Devices/hooks') +vi.mock('../../../../organisms/Devices/hooks/useLastRunCommandKey') +vi.mock('../../../../organisms/RunTimeControl/hooks') vi.mock( - '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' + '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) -vi.mock('../../../organisms/RunTimeControl/hooks') -vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol') -vi.mock('../../../redux/discovery') -vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal') -vi.mock('../../../organisms/OpenDoorAlertModal') -vi.mock('../../../resources/runs') -vi.mock('../../../redux/config') -vi.mock('../../../organisms/ErrorRecoveryFlows') -vi.mock('../../../organisms/Devices/hooks/useLastRunCommand') -vi.mock('../../../organisms/InterventionModal') +vi.mock('../../../../organisms/RunTimeControl/hooks') +vi.mock('../../../../organisms/ODD/RunningProtocol') +vi.mock('../../../../redux/discovery') +vi.mock('../../../../organisms/ODD/RunningProtocol/CancelingRunModal') +vi.mock('../../../../organisms/OpenDoorAlertModal') +vi.mock('../../../../resources/runs') +vi.mock('../../../../redux/config') +vi.mock('../../../../organisms/ErrorRecoveryFlows') +vi.mock('../../../../organisms/Devices/hooks/useLastRunCommand') +vi.mock('../../../../organisms/InterventionModal') const RUN_ID = 'run_id' const ROBOT_NAME = 'otie' @@ -149,7 +149,10 @@ describe('RunningProtocol', () => { .calledWith(RUN_ID) .thenReturn(mockRobotSideAnalysis) when(vi.mocked(useNotifyAllCommandsQuery)) - .calledWith(RUN_ID, { cursor: null, pageLength: 1 }) + .calledWith(RUN_ID, { + cursor: null, + pageLength: 1, + }) .thenReturn(mockUseAllCommandsResponseNonDeterministic) vi.mocked(useLastRunCommand).mockReturnValue({ key: 'FAKE_COMMAND_KEY', diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/ODD/RunningProtocol/index.tsx similarity index 90% rename from app/src/pages/RunningProtocol/index.tsx rename to app/src/pages/ODD/RunningProtocol/index.tsx index de6cfc58994..02d961a0b3a 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/ODD/RunningProtocol/index.tsx @@ -26,38 +26,38 @@ import { RUN_STATUS_BLOCKED_BY_OPEN_DOOR, } from '@opentrons/api-client' -import { StepMeter } from '../../atoms/StepMeter' -import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useNotifyRunQuery } from '../../resources/runs' +import { StepMeter } from '../../../atoms/StepMeter' +import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useNotifyRunQuery } from '../../../resources/runs' import { InterventionModal, useInterventionModal, -} from '../../organisms/InterventionModal' +} from '../../../organisms/InterventionModal' import { useRunStatus, useRunTimestamps, -} from '../../organisms/RunTimeControl/hooks' +} from '../../../organisms/RunTimeControl/hooks' import { CurrentRunningProtocolCommand, RunningProtocolCommandList, RunningProtocolSkeleton, -} from '../../organisms/OnDeviceDisplay/RunningProtocol' +} from '../../../organisms/ODD/RunningProtocol' import { useTrackProtocolRunEvent, useRobotAnalyticsData, useRobotType, -} from '../../organisms/Devices/hooks' -import { CancelingRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal' -import { ConfirmCancelRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal' -import { getLocalRobot } from '../../redux/discovery' -import { OpenDoorAlertModal } from '../../organisms/OpenDoorAlertModal' +} from '../../../organisms/Devices/hooks' +import { CancelingRunModal } from '../../../organisms/ODD/RunningProtocol/CancelingRunModal' +import { ConfirmCancelRunModal } from '../../../organisms/ODD/RunningProtocol/ConfirmCancelRunModal' +import { getLocalRobot } from '../../../redux/discovery' +import { OpenDoorAlertModal } from '../../../organisms/OpenDoorAlertModal' import { useErrorRecoveryFlows, ErrorRecoveryFlows, -} from '../../organisms/ErrorRecoveryFlows' -import { useLastRunCommand } from '../../organisms/Devices/hooks/useLastRunCommand' +} from '../../../organisms/ErrorRecoveryFlows' +import { useLastRunCommand } from '../../../organisms/Devices/hooks/useLastRunCommand' -import type { OnDeviceRouteParams } from '../../App/types' +import type { OnDeviceRouteParams } from '../../../App/types' const RUN_STATUS_REFETCH_INTERVAL = 5000 const LIVE_RUN_COMMANDS_POLL_MS = 3000 diff --git a/app/src/pages/UpdateRobot/UpdateRobot.tsx b/app/src/pages/ODD/UpdateRobot/UpdateRobot.tsx similarity index 84% rename from app/src/pages/UpdateRobot/UpdateRobot.tsx rename to app/src/pages/ODD/UpdateRobot/UpdateRobot.tsx index 413665365a0..0a3929e04f9 100644 --- a/app/src/pages/UpdateRobot/UpdateRobot.tsx +++ b/app/src/pages/ODD/UpdateRobot/UpdateRobot.tsx @@ -5,22 +5,22 @@ import { useTranslation } from 'react-i18next' import { Flex, SPACING, DIRECTION_ROW } from '@opentrons/components' -import { getLocalRobot } from '../../redux/discovery' +import { getLocalRobot } from '../../../redux/discovery' import { getRobotUpdateAvailable, clearRobotUpdateSession, -} from '../../redux/robot-update' -import { useDispatchStartRobotUpdate } from '../../redux/robot-update/hooks' -import { UNREACHABLE } from '../../redux/discovery/constants' +} from '../../../redux/robot-update' +import { useDispatchStartRobotUpdate } from '../../../redux/robot-update/hooks' +import { UNREACHABLE } from '../../../redux/discovery/constants' -import { MediumButton } from '../../atoms/buttons' +import { MediumButton } from '../../../atoms/buttons' import { UpdateRobotSoftware, NoUpdateFound, ErrorUpdateSoftware, -} from '../../organisms/UpdateRobotSoftware' +} from '../../../organisms/UpdateRobotSoftware' -import type { State, Dispatch } from '../../redux/types' +import type { State, Dispatch } from '../../../redux/types' export function UpdateRobot(): JSX.Element { const navigate = useNavigate() diff --git a/app/src/pages/UpdateRobot/UpdateRobotDuringOnboarding.tsx b/app/src/pages/ODD/UpdateRobot/UpdateRobotDuringOnboarding.tsx similarity index 88% rename from app/src/pages/UpdateRobot/UpdateRobotDuringOnboarding.tsx rename to app/src/pages/ODD/UpdateRobot/UpdateRobotDuringOnboarding.tsx index 60f4155273e..3efcc4bace2 100644 --- a/app/src/pages/UpdateRobot/UpdateRobotDuringOnboarding.tsx +++ b/app/src/pages/ODD/UpdateRobot/UpdateRobotDuringOnboarding.tsx @@ -5,27 +5,27 @@ import { useTranslation } from 'react-i18next' import { Flex, SPACING, DIRECTION_ROW } from '@opentrons/components' -import { useDispatchStartRobotUpdate } from '../../redux/robot-update/hooks' +import { useDispatchStartRobotUpdate } from '../../../redux/robot-update/hooks' -import { getLocalRobot } from '../../redux/discovery' +import { getLocalRobot } from '../../../redux/discovery' import { getRobotUpdateAvailable, clearRobotUpdateSession, -} from '../../redux/robot-update' -import { UNREACHABLE } from '../../redux/discovery/constants' +} from '../../../redux/robot-update' +import { UNREACHABLE } from '../../../redux/discovery/constants' import { getOnDeviceDisplaySettings, updateConfigValue, -} from '../../redux/config' -import { MediumButton } from '../../atoms/buttons' +} from '../../../redux/config' +import { MediumButton } from '../../../atoms/buttons' import { UpdateRobotSoftware, CheckUpdates, NoUpdateFound, ErrorUpdateSoftware, -} from '../../organisms/UpdateRobotSoftware' +} from '../../../organisms/UpdateRobotSoftware' -import type { Dispatch, State } from '../../redux/types' +import type { Dispatch, State } from '../../../redux/types' const CHECK_UPDATES_DURATION = 10000 // Note: kj 1/10/2023 Currently set 10 sec later we may use a status from state diff --git a/app/src/pages/UpdateRobot/__tests__/UpdateRobot.test.tsx b/app/src/pages/ODD/UpdateRobot/__tests__/UpdateRobot.test.tsx similarity index 86% rename from app/src/pages/UpdateRobot/__tests__/UpdateRobot.test.tsx rename to app/src/pages/ODD/UpdateRobot/__tests__/UpdateRobot.test.tsx index d7ff6bf9a72..de37b8c36c8 100644 --- a/app/src/pages/UpdateRobot/__tests__/UpdateRobot.test.tsx +++ b/app/src/pages/ODD/UpdateRobot/__tests__/UpdateRobot.test.tsx @@ -4,19 +4,19 @@ import { MemoryRouter } from 'react-router-dom' import { when } from 'vitest-when' import { screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' -import * as RobotUpdate from '../../../redux/robot-update' -import type { RobotUpdateSession } from '../../../redux/robot-update/types' -import { getLocalRobot } from '../../../redux/discovery' +import * as RobotUpdate from '../../../../redux/robot-update' +import type { RobotUpdateSession } from '../../../../redux/robot-update/types' +import { getLocalRobot } from '../../../../redux/discovery' import { UpdateRobot } from '../UpdateRobot' -import type { State } from '../../../redux/types' +import type { State } from '../../../../redux/types' -vi.mock('../../../redux/discovery') -vi.mock('../../../redux/robot-update') +vi.mock('../../../../redux/discovery') +vi.mock('../../../../redux/robot-update') const MOCK_STATE: State = { discovery: { diff --git a/app/src/pages/UpdateRobot/__tests__/UpdateRobotDuringOnboarding.test.tsx b/app/src/pages/ODD/UpdateRobot/__tests__/UpdateRobotDuringOnboarding.test.tsx similarity index 90% rename from app/src/pages/UpdateRobot/__tests__/UpdateRobotDuringOnboarding.test.tsx rename to app/src/pages/ODD/UpdateRobot/__tests__/UpdateRobotDuringOnboarding.test.tsx index a4e7e94af6c..46711fa15a3 100644 --- a/app/src/pages/UpdateRobot/__tests__/UpdateRobotDuringOnboarding.test.tsx +++ b/app/src/pages/ODD/UpdateRobot/__tests__/UpdateRobotDuringOnboarding.test.tsx @@ -2,19 +2,19 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { act, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' -import * as RobotUpdate from '../../../redux/robot-update' -import type { RobotUpdateSession } from '../../../redux/robot-update/types' -import { getLocalRobot } from '../../../redux/discovery' +import * as RobotUpdate from '../../../../redux/robot-update' +import type { RobotUpdateSession } from '../../../../redux/robot-update/types' +import { getLocalRobot } from '../../../../redux/discovery' import { UpdateRobotDuringOnboarding } from '../UpdateRobotDuringOnboarding' -import type { State } from '../../../redux/types' +import type { State } from '../../../../redux/types' -vi.mock('../../../redux/discovery') -vi.mock('../../../redux/robot-update') +vi.mock('../../../../redux/discovery') +vi.mock('../../../../redux/robot-update') const MOCK_STATE: State = { discovery: { diff --git a/app/src/pages/Welcome/__tests__/Welcome.test.tsx b/app/src/pages/ODD/Welcome/__tests__/Welcome.test.tsx similarity index 92% rename from app/src/pages/Welcome/__tests__/Welcome.test.tsx rename to app/src/pages/ODD/Welcome/__tests__/Welcome.test.tsx index 756b7bcb4b5..c591c1379d0 100644 --- a/app/src/pages/Welcome/__tests__/Welcome.test.tsx +++ b/app/src/pages/ODD/Welcome/__tests__/Welcome.test.tsx @@ -3,9 +3,9 @@ import { vi, it, describe, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' -import { renderWithProviders } from '../../../__testing-utils__' +import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../i18n' +import { i18n } from '../../../../i18n' import { Welcome } from '..' import type { NavigateFunction } from 'react-router-dom' diff --git a/app/src/pages/Welcome/index.tsx b/app/src/pages/ODD/Welcome/index.tsx similarity index 91% rename from app/src/pages/Welcome/index.tsx rename to app/src/pages/ODD/Welcome/index.tsx index d43c9e9e054..f4564904f0e 100644 --- a/app/src/pages/Welcome/index.tsx +++ b/app/src/pages/ODD/Welcome/index.tsx @@ -10,9 +10,9 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' -import { MediumButton } from '../../atoms/buttons' +import { MediumButton } from '../../../atoms/buttons' -import screenImage from '../../assets/images/on-device-display/welcome_background.png' +import screenImage from '../../../assets/images/on-device-display/welcome_background.png' const IMAGE_ALT = 'Welcome screen background image' diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts deleted file mode 100644 index e28f80e805c..00000000000 --- a/app/src/pages/Protocols/hooks/index.ts +++ /dev/null @@ -1,382 +0,0 @@ -import last from 'lodash/last' -import { - useInstrumentsQuery, - useModulesQuery, - useProtocolAnalysisAsDocumentQuery, - useProtocolQuery, -} from '@opentrons/react-api-client' -import { - FLEX_ROBOT_TYPE, - FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, - getCutoutIdForSlotName, - getDeckDefFromRobotType, - getCutoutFixtureIdsForModuleModel, - getCutoutFixturesForModuleModel, - FLEX_MODULE_ADDRESSABLE_AREAS, - getModuleType, - FLEX_USB_MODULE_ADDRESSABLE_AREAS, - MAGNETIC_BLOCK_TYPE, -} from '@opentrons/shared-data' -import { getLabwareSetupItemGroups } from '../utils' -import { getProtocolUsesGripper } from '../../../organisms/ProtocolSetupInstruments/utils' -import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' -import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' - -import type { - CompletedProtocolAnalysis, - CutoutFixtureId, - CutoutId, - ModuleModel, - PipetteName, - ProtocolAnalysisOutput, - RobotType, - RunTimeParameter, -} from '@opentrons/shared-data' -import type { LabwareSetupItem } from '../utils' - -export interface ProtocolPipette { - hardwareType: 'pipette' - pipetteName: PipetteName - mount: 'left' | 'right' - connected: boolean -} - -interface ProtocolModule { - hardwareType: 'module' - moduleModel: ModuleModel - slot: string - connected: boolean - hasSlotConflict: boolean -} - -interface ProtocolGripper { - hardwareType: 'gripper' - connected: boolean -} - -export interface ProtocolFixture { - hardwareType: 'fixture' - cutoutFixtureId: CutoutFixtureId | null - location: { cutout: CutoutId } - hasSlotConflict: boolean -} - -export type ProtocolHardware = - | ProtocolPipette - | ProtocolModule - | ProtocolGripper - | ProtocolFixture - -const DECK_CONFIG_REFETCH_INTERVAL = 5000 - -export const useRequiredProtocolHardwareFromAnalysis = ( - analysis: CompletedProtocolAnalysis | null -): { requiredProtocolHardware: ProtocolHardware[]; isLoading: boolean } => { - const { - data: attachedModulesData, - isLoading: isLoadingModules, - } = useModulesQuery() - const attachedModules = attachedModulesData?.data ?? [] - - const { - data: attachedInstrumentsData, - isLoading: isLoadingInstruments, - } = useInstrumentsQuery() - const attachedInstruments = attachedInstrumentsData?.data ?? [] - - const robotType = FLEX_ROBOT_TYPE - const deckDef = getDeckDefFromRobotType(robotType) - const deckConfig = - useNotifyDeckConfigurationQuery({ - refetchInterval: DECK_CONFIG_REFETCH_INTERVAL, - })?.data ?? [] - const deckConfigCompatibility = useDeckConfigurationCompatibility( - robotType, - analysis - ) - - if (analysis == null || analysis?.status !== 'completed') { - return { requiredProtocolHardware: [], isLoading: true } - } - - const requiredGripper: ProtocolGripper[] = getProtocolUsesGripper(analysis) - ? [ - { - hardwareType: 'gripper', - connected: - attachedInstruments.some(i => i.instrumentType === 'gripper') ?? - false, - }, - ] - : [] - - const requiredModules: ProtocolModule[] = analysis.modules - // remove magnetic blocks, they're handled by required fixtures - .filter(m => getModuleType(m.model) !== MAGNETIC_BLOCK_TYPE) - .map(({ location, model }) => { - const cutoutIdForSlotName = getCutoutIdForSlotName( - location.slotName, - deckDef - ) - const moduleFixtures = getCutoutFixturesForModuleModel(model, deckDef) - - const configuredModuleSerialNumber = - deckConfig.find( - ({ cutoutId, cutoutFixtureId }) => - cutoutId === cutoutIdForSlotName && - moduleFixtures.map(mf => mf.id).includes(cutoutFixtureId) - )?.opentronsModuleSerialNumber ?? null - const isConnected = moduleFixtures.every( - mf => mf.expectOpentronsModuleSerialNumber - ) - ? attachedModules.some( - m => - m.moduleModel === model && - m.serialNumber === configuredModuleSerialNumber - ) - : true - return { - hardwareType: 'module', - moduleModel: model, - slot: location.slotName, - connected: isConnected, - hasSlotConflict: deckConfig.some( - ({ cutoutId, cutoutFixtureId }) => - cutoutId === getCutoutIdForSlotName(location.slotName, deckDef) && - !getCutoutFixtureIdsForModuleModel(model).includes(cutoutFixtureId) - ), - } - }) - - const requiredPipettes: ProtocolPipette[] = analysis.pipettes.map( - ({ mount, pipetteName }) => ({ - hardwareType: 'pipette', - pipetteName: pipetteName, - mount: mount, - connected: - attachedInstruments.some( - i => - i.instrumentType === 'pipette' && - i.ok && - i.mount === mount && - i.instrumentName === pipetteName - ) ?? false, - }) - ) - - // fixture includes at least 1 required addressableArea AND it doesn't ONLY include a single slot addressableArea - const requiredDeckConfigCompatibility = deckConfigCompatibility.filter( - ({ requiredAddressableAreas }) => { - const atLeastOneAA = requiredAddressableAreas.length > 0 - const notOnlySingleSlot = !( - requiredAddressableAreas.length === 1 && - FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(requiredAddressableAreas[0]) - ) - return atLeastOneAA && notOnlySingleSlot - } - ) - - const requiredFixtures = requiredDeckConfigCompatibility - // filter out all fixtures that only provide usb module addressable areas - // as they're handled in the requiredModules section via hardwareType === 'module' - .filter( - ({ requiredAddressableAreas }) => - !requiredAddressableAreas.every(modAA => - FLEX_USB_MODULE_ADDRESSABLE_AREAS.includes(modAA) - ) - ) - .map(({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ - hardwareType: 'fixture' as const, - cutoutFixtureId: compatibleCutoutFixtureIds[0], - location: { cutout: cutoutId }, - hasSlotConflict: - cutoutFixtureId != null && - !compatibleCutoutFixtureIds.includes(cutoutFixtureId), - })) - - return { - requiredProtocolHardware: [ - ...requiredPipettes, - ...requiredModules, - ...requiredGripper, - ...requiredFixtures, - ], - isLoading: isLoadingInstruments || isLoadingModules, - } -} - -/** - * Returns an array of RunTimeParameters objects that are optional by the given protocol ID. - * - * @param {string} protocolId The ID of the protocol for which required hardware is being retrieved. - * @returns {RunTimeParameters[]} An array of RunTimeParameters objects that are required by the given protocol ID. - */ - -export const useRunTimeParameters = ( - protocolId: string -): RunTimeParameter[] => { - const { data: protocolData } = useProtocolQuery(protocolId) - const { data: analysis } = useProtocolAnalysisAsDocumentQuery( - protocolId, - last(protocolData?.data.analysisSummaries)?.id ?? null, - { enabled: protocolData != null } - ) - - return analysis?.runTimeParameters ?? [] -} - -/** - * Returns an array of ProtocolHardware objects that are required by the given protocol ID. - * - * @param {string} protocolId The ID of the protocol for which required hardware is being retrieved. - * @returns {ProtocolHardware[]} An array of ProtocolHardware objects that are required by the given protocol ID. - */ - -export const useRequiredProtocolHardware = ( - protocolId: string -): { requiredProtocolHardware: ProtocolHardware[]; isLoading: boolean } => { - const { data: protocolData } = useProtocolQuery(protocolId) - const { data: analysis } = useProtocolAnalysisAsDocumentQuery( - protocolId, - last(protocolData?.data.analysisSummaries)?.id ?? null, - { enabled: protocolData != null } - ) - - return useRequiredProtocolHardwareFromAnalysis(analysis ?? null) -} - -/** - * Returns an array of LabwareSetupItem objects that are required by the given protocol ID. - * - * @param {string} protocolId The ID of the protocol for which required labware setup items are being retrieved. - * @returns {LabwareSetupItem[]} An array of LabwareSetupItem objects that are required by the given protocol ID. - */ -export const useRequiredProtocolLabware = ( - protocolId: string -): LabwareSetupItem[] => { - const { data: protocolData } = useProtocolQuery(protocolId) - const { - data: mostRecentAnalysis, - } = useProtocolAnalysisAsDocumentQuery( - protocolId, - last(protocolData?.data.analysisSummaries)?.id ?? null, - { enabled: protocolData != null } - ) - const commands = - (mostRecentAnalysis as CompletedProtocolAnalysis)?.commands ?? [] - const { onDeckItems, offDeckItems } = getLabwareSetupItemGroups(commands) - return [...onDeckItems, ...offDeckItems] -} - -/** - * Returns an array of ProtocolHardware objects that are required by the given protocol ID, - * but not currently connected. - * - * @param {ProtocolHardware[]} requiredProtocolHardware An array of ProtocolHardware objects that are required by a protocol. - * @param {boolean} isLoading A boolean determining whether any required protocol hardware is loading. - * @returns {ProtocolHardware[]} An array of ProtocolHardware objects that are required by the given protocol ID, but not currently connected. - */ - -const useMissingProtocolHardwareFromRequiredProtocolHardware = ( - requiredProtocolHardware: ProtocolHardware[], - isLoading: boolean, - robotType: RobotType, - protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null -): { - missingProtocolHardware: ProtocolHardware[] - conflictedSlots: string[] - isLoading: boolean -} => { - const deckConfigCompatibility = useDeckConfigurationCompatibility( - robotType, - protocolAnalysis - ) - // determine missing or conflicted hardware - return { - missingProtocolHardware: [ - ...requiredProtocolHardware.filter( - hardware => 'connected' in hardware && !hardware.connected - ), - ...deckConfigCompatibility - .filter( - ({ - cutoutFixtureId, - compatibleCutoutFixtureIds, - requiredAddressableAreas, - }) => - cutoutFixtureId != null && - !compatibleCutoutFixtureIds.some(id => id === cutoutFixtureId) && - !FLEX_MODULE_ADDRESSABLE_AREAS.some(modAA => - requiredAddressableAreas.includes(modAA) - ) // modules are already included via requiredProtocolHardware - ) - .map(({ compatibleCutoutFixtureIds, cutoutId }) => ({ - hardwareType: 'fixture' as const, - cutoutFixtureId: compatibleCutoutFixtureIds[0], - location: { cutout: cutoutId }, - hasSlotConflict: true, - })), - ], - conflictedSlots: requiredProtocolHardware - .filter( - (hardware): hardware is ProtocolModule | ProtocolFixture => - (hardware.hardwareType === 'module' || - hardware.hardwareType === 'fixture') && - hardware.hasSlotConflict - ) - .map( - hardware => - hardware.hardwareType === 'module' - ? hardware.slot // module - : hardware.location.cutout // fixture - ), - isLoading, - } -} - -export const useMissingProtocolHardwareFromAnalysis = ( - robotType: RobotType, - analysis: CompletedProtocolAnalysis | null -): { - missingProtocolHardware: ProtocolHardware[] - conflictedSlots: string[] - isLoading: boolean -} => { - const { - requiredProtocolHardware, - isLoading, - } = useRequiredProtocolHardwareFromAnalysis(analysis) - - return useMissingProtocolHardwareFromRequiredProtocolHardware( - requiredProtocolHardware, - isLoading, - robotType, - analysis ?? null - ) -} - -export const useMissingProtocolHardware = ( - protocolId: string -): { - missingProtocolHardware: ProtocolHardware[] - conflictedSlots: string[] - isLoading: boolean -} => { - const { data: protocolData } = useProtocolQuery(protocolId) - const { data: analysis } = useProtocolAnalysisAsDocumentQuery( - protocolId, - last(protocolData?.data.analysisSummaries)?.id ?? null, - { enabled: protocolData != null } - ) - const { - requiredProtocolHardware, - isLoading, - } = useRequiredProtocolHardwareFromAnalysis(analysis ?? null) - - return useMissingProtocolHardwareFromRequiredProtocolHardware( - requiredProtocolHardware, - isLoading, - FLEX_ROBOT_TYPE, - analysis ?? null - ) -} diff --git a/app/src/redux/custom-labware/__fixtures__/index.ts b/app/src/redux/custom-labware/__fixtures__/index.ts index 56233d506fd..3b2da5d77df 100644 --- a/app/src/redux/custom-labware/__fixtures__/index.ts +++ b/app/src/redux/custom-labware/__fixtures__/index.ts @@ -1,5 +1,5 @@ import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LabwareWellGroupProperties } from '../../../pages/Labware/types' +import type { LabwareWellGroupProperties } from '../../../pages/Desktop/Labware/types' import type * as Types from '../types' export const mockDefinition: LabwareDefinition2 = { diff --git a/app/src/redux/protocol-storage/selectors.ts b/app/src/redux/protocol-storage/selectors.ts index 71405c46b4a..4684443918b 100644 --- a/app/src/redux/protocol-storage/selectors.ts +++ b/app/src/redux/protocol-storage/selectors.ts @@ -16,7 +16,7 @@ export const getStoredProtocols: ( export const getStoredProtocol: ( state: State, - protocolKey?: string + protocolKey?: string | null ) => StoredProtocolData | null = (state, protocolKey) => protocolKey != null ? state.protocolStorage.filesByProtocolKey[protocolKey] ?? null diff --git a/app/src/redux/robot-api/http.ts b/app/src/redux/robot-api/http.ts index 7dd41b87da6..63f961acf85 100644 --- a/app/src/redux/robot-api/http.ts +++ b/app/src/redux/robot-api/http.ts @@ -5,6 +5,7 @@ import mapValues from 'lodash/mapValues' import toString from 'lodash/toString' import omitBy from 'lodash/omitBy' import inRange from 'lodash/inRange' +import type { AxiosError } from 'axios' import { OPENTRONS_USB } from '../../redux/discovery' import { appShellRequestor } from '../../redux/shell/remote' @@ -62,15 +63,27 @@ export function fetchRobotApi( url, data: options.body, }) + .then(response => ({ + isError: false as const, + response, + })) + .catch(err => ({ + isError: true as const, + ...(err as AxiosError), + })) ).pipe( - map(response => ({ + map(result => ({ host, path, method, - body: response?.data, - status: response?.status, + body: result?.response?.data, + // FIXME(sf) this doesn't seem right, but also the type interface isn't written to allow for request + // failures that don't come from valid connections + status: result?.response?.status ?? 444, // appShellRequestor eventually calls axios.request, which doesn't provide an ok boolean in the response - ok: inRange(response?.status, 200, 300), + ok: result.isError + ? false + : inRange(result?.response?.status, 200, 300), })) ) : from(fetch(url, options)).pipe( diff --git a/app/src/redux/robot-controls/actions.ts b/app/src/redux/robot-controls/actions.ts index 816475a1738..1d29782b964 100644 --- a/app/src/redux/robot-controls/actions.ts +++ b/app/src/redux/robot-controls/actions.ts @@ -66,6 +66,9 @@ type HomeActionCreator = (( ) => Types.HomeAction) & ((robotName: string, target: 'pipette', mount: Mount) => Types.HomeAction) +/** + * @deprecated: Prefer performing single robot commands via maintenance run. See useRobotControlCommands. + */ export const home: HomeActionCreator = ( robotName: string, target: 'robot' | 'pipette', @@ -98,6 +101,9 @@ export const homeFailure = ( meta, }) +/** + * @deprecated: Prefer performing single robot commands via maintenance run. See useRobotControlCommands. + */ export const move = ( robotName: string, position: Types.MovePosition, diff --git a/app/src/redux/robot-update/hooks.ts b/app/src/redux/robot-update/hooks.ts index 69f4b0a60c0..c37f39d044f 100644 --- a/app/src/redux/robot-update/hooks.ts +++ b/app/src/redux/robot-update/hooks.ts @@ -1,6 +1,8 @@ -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { startRobotUpdate, clearRobotUpdateSession } from './actions' -import type { Dispatch } from '../types' +import { getRobotUpdateDisplayInfo } from './selectors' + +import type { Dispatch, State } from '../types' type DispatchStartRobotUpdate = ( robotName: string, @@ -21,3 +23,11 @@ export function useDispatchStartRobotUpdate(): DispatchStartRobotUpdate { return dispatchStartRobotUpdate } + +// Whether the robot is on a different version of software than the current app. +export function useIsRobotOnWrongVersionOfSoftware(robotName: string): boolean { + return ['upgrade', 'downgrade'].includes( + useSelector((state: State) => getRobotUpdateDisplayInfo(state, robotName)) + ?.autoUpdateAction + ) +} diff --git a/app/src/redux/robot-update/index.ts b/app/src/redux/robot-update/index.ts index 497c8935180..a7e12bf4675 100644 --- a/app/src/redux/robot-update/index.ts +++ b/app/src/redux/robot-update/index.ts @@ -1,3 +1,4 @@ export * from './actions' export * from './constants' export * from './selectors' +export { useIsRobotOnWrongVersionOfSoftware } from './hooks' diff --git a/app/src/redux/shell/remote.ts b/app/src/redux/shell/remote.ts index 6692c6dca04..4af9cc5a3ce 100644 --- a/app/src/redux/shell/remote.ts +++ b/app/src/redux/shell/remote.ts @@ -57,7 +57,11 @@ export async function appShellRequestor( : data const configProxy = { ...config, data: formDataProxy } - return await remote.ipcRenderer.invoke('usb:request', configProxy) + const result = await remote.ipcRenderer.invoke('usb:request', configProxy) + if (result?.error != null) { + throw result.error + } + return result } interface CallbackStore { diff --git a/app/src/resources/__tests__/useNotifyDataReady.ts b/app/src/resources/__tests__/useNotifyDataReady.test.ts similarity index 76% rename from app/src/resources/__tests__/useNotifyDataReady.ts rename to app/src/resources/__tests__/useNotifyDataReady.test.ts index 3fe27de0d77..3ac0130a85d 100644 --- a/app/src/resources/__tests__/useNotifyDataReady.ts +++ b/app/src/resources/__tests__/useNotifyDataReady.test.ts @@ -33,7 +33,6 @@ const MOCK_OPTIONS: QueryOptionsWithPolling = { describe('useNotifyDataReady', () => { let mockDispatch: Mock let mockTrackEvent: Mock - let mockHTTPRefetch: Mock beforeEach(() => { mockDispatch = vi.fn() @@ -52,7 +51,7 @@ describe('useNotifyDataReady', () => { vi.clearAllMocks() }) - it('should trigger an HTTP refetch and subscribe action on a successful initial mount', () => { + it('should return queryOptionsNotify and shouldRefetch on a successful initial mount', () => { const { result } = renderHook(() => useNotifyDataReady({ topic: MOCK_TOPIC, @@ -60,6 +59,7 @@ describe('useNotifyDataReady', () => { } as any) ) expect(result.current.shouldRefetch).toEqual(true) + expect(result.current.queryOptionsNotify).toBeDefined() expect(mockDispatch).toHaveBeenCalledWith( notifySubscribeAction(MOCK_HOST_CONFIG.hostname, MOCK_TOPIC) ) @@ -73,7 +73,7 @@ describe('useNotifyDataReady', () => { options: { ...MOCK_OPTIONS, forceHttpPolling: true }, } as any) ) - expect(result.current.shouldRefetch).toEqual(true) + expect(result.current.shouldRefetch).toEqual(false) expect(appShellListener).not.toHaveBeenCalled() expect(mockDispatch).not.toHaveBeenCalled() }) @@ -85,7 +85,7 @@ describe('useNotifyDataReady', () => { options: { ...MOCK_OPTIONS, enabled: false }, } as any) ) - expect(result.current.shouldRefetch).toEqual(true) + expect(result.current.shouldRefetch).toEqual(false) expect(appShellListener).not.toHaveBeenCalled() expect(mockDispatch).not.toHaveBeenCalled() }) @@ -97,12 +97,12 @@ describe('useNotifyDataReady', () => { options: { ...MOCK_OPTIONS, staleTime: Infinity }, } as any) ) - expect(result.current.shouldRefetch).toEqual(true) + expect(result.current.shouldRefetch).toEqual(false) expect(appShellListener).not.toHaveBeenCalled() expect(mockDispatch).not.toHaveBeenCalled() }) - it('should set HTTP refetch to always if there is an error', () => { + it('should set shouldRefetch to false if there is an error', () => { vi.mocked(useHost).mockReturnValue({ hostname: null } as any) const errorSpy = vi.spyOn(console, 'error') errorSpy.mockImplementation(() => {}) @@ -110,15 +110,14 @@ describe('useNotifyDataReady', () => { const { result } = renderHook(() => useNotifyDataReady({ topic: MOCK_TOPIC, - setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) - expect(result.current.shouldRefetch).toEqual(true) + expect(result.current.shouldRefetch).toEqual(false) }) - it('should return set HTTP refetch to always and fire an analytics reporting event if the connection was refused', () => { + it('should set shouldRefetch to false and fire an analytics reporting event if the connection was refused', () => { vi.mocked(appShellListener).mockImplementation(function ({ callback, }): any { @@ -128,16 +127,15 @@ describe('useNotifyDataReady', () => { const { rerender, result } = renderHook(() => useNotifyDataReady({ topic: MOCK_TOPIC, - setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) expect(mockTrackEvent).toHaveBeenCalled() rerender() - expect(result.current.shouldRefetch).toEqual(true) + expect(result.current.shouldRefetch).toEqual(false) }) - it('should trigger a single HTTP refetch if the refetch flag was returned', () => { + it('should set shouldRefetch to true if the refetch flag was returned', () => { vi.mocked(appShellListener).mockImplementation(function ({ callback, }): any { @@ -147,7 +145,6 @@ describe('useNotifyDataReady', () => { const { rerender, result } = renderHook(() => useNotifyDataReady({ topic: MOCK_TOPIC, - setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -155,7 +152,7 @@ describe('useNotifyDataReady', () => { expect(result.current.shouldRefetch).toEqual(true) }) - it('should trigger a single HTTP refetch if the unsubscribe flag was returned', () => { + it('should set shouldRefetch to true if the unsubscribe flag was returned', () => { vi.mocked(appShellListener).mockImplementation(function ({ callback, }): any { @@ -210,7 +207,38 @@ describe('useNotifyDataReady', () => { } as any) ) - expect(result.current.shouldRefetch).toEqual(true) + expect(result.current.shouldRefetch).toEqual(false) expect(appShellListener).not.toHaveBeenCalled() }) + + it('should return queryOptionsNotify with modified onSettled and refetchInterval', () => { + const { result } = renderHook(() => + useNotifyDataReady({ + topic: MOCK_TOPIC, + options: { + ...MOCK_OPTIONS, + onSettled: vi.fn(), + refetchInterval: 5000, + }, + } as any) + ) + expect(result.current.queryOptionsNotify.onSettled).toBeDefined() + expect(result.current.queryOptionsNotify.refetchInterval).toBe(false) + }) + + it('should call the original onSettled function when notifications are disabled', () => { + const mockOnSettled = vi.fn() + const { result } = renderHook(() => + useNotifyDataReady({ + topic: MOCK_TOPIC, + options: { + ...MOCK_OPTIONS, + forceHttpPolling: true, + onSettled: mockOnSettled, + }, + } as any) + ) + result.current.queryOptionsNotify.onSettled?.(undefined, null) + expect(mockOnSettled).toHaveBeenCalled() + }) }) diff --git a/app/src/resources/client_data/recovery/useNotifyClientDataRecovery.ts b/app/src/resources/client_data/recovery/useNotifyClientDataRecovery.ts index a93553fa3fa..4371a0f3fa2 100644 --- a/app/src/resources/client_data/recovery/useNotifyClientDataRecovery.ts +++ b/app/src/resources/client_data/recovery/useNotifyClientDataRecovery.ts @@ -14,19 +14,19 @@ export function useNotifyClientDataRecovery( AxiosError > = {} ): UseQueryResult, AxiosError> { - const { notifyOnSettled, shouldRefetch } = useNotifyDataReady({ + const { shouldRefetch, queryOptionsNotify } = useNotifyDataReady({ topic: `robot-server/clientData/${KEYS.ERROR_RECOVERY}`, options, }) const httpQueryResult = useClientData( KEYS.ERROR_RECOVERY, - { - ...options, - enabled: options?.enabled !== false && shouldRefetch, - onSettled: notifyOnSettled, - } + queryOptionsNotify ) + if (shouldRefetch) { + void httpQueryResult.refetch() + } + return httpQueryResult } diff --git a/app/src/resources/deck_configuration/useNotifyDeckConfigurationQuery.ts b/app/src/resources/deck_configuration/useNotifyDeckConfigurationQuery.ts index a4e999bfbaa..55cd6e383d1 100644 --- a/app/src/resources/deck_configuration/useNotifyDeckConfigurationQuery.ts +++ b/app/src/resources/deck_configuration/useNotifyDeckConfigurationQuery.ts @@ -9,16 +9,16 @@ import type { QueryOptionsWithPolling } from '../useNotifyDataReady' export function useNotifyDeckConfigurationQuery( options: QueryOptionsWithPolling = {} ): UseQueryResult { - const { notifyOnSettled, shouldRefetch } = useNotifyDataReady({ + const { shouldRefetch, queryOptionsNotify } = useNotifyDataReady({ topic: 'robot-server/deck_configuration', options, }) - const httpQueryResult = useDeckConfigurationQuery({ - ...options, - enabled: options?.enabled !== false && shouldRefetch, - onSettled: notifyOnSettled, - }) + const httpQueryResult = useDeckConfigurationQuery(queryOptionsNotify) + + if (shouldRefetch) { + void httpQueryResult.refetch() + } return httpQueryResult } diff --git a/app/src/resources/instruments/__tests__/hooks.test.ts b/app/src/resources/instruments/__tests__/hooks.test.ts new file mode 100644 index 00000000000..ab21c81525d --- /dev/null +++ b/app/src/resources/instruments/__tests__/hooks.test.ts @@ -0,0 +1,178 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useIsOEMMode } from '../../robot-settings/hooks' + +import { + useGripperDisplayName, + usePipetteModelSpecs, + usePipetteNameSpecs, + usePipetteSpecsV2, +} from '../hooks' + +import type { PipetteV2Specs } from '@opentrons/shared-data' + +vi.mock('../../robot-settings/hooks') + +const BRANDED_P1000_FLEX_DISPLAY_NAME = 'Flex 1-Channel 1000 μL' +const ANONYMOUS_P1000_FLEX_DISPLAY_NAME = '1-Channel 1000 μL' + +const mockP1000V2Specs = { + $otSharedSchema: '#/pipette/schemas/2/pipetteGeometrySchema.json', + availableSensors: { + sensors: ['pressure', 'capacitive', 'environment'], + capacitive: { count: 1 }, + environment: { count: 1 }, + pressure: { count: 1 }, + }, + backCompatNames: [], + backlashDistance: 0.1, + channels: 1, + displayCategory: 'FLEX', + displayName: BRANDED_P1000_FLEX_DISPLAY_NAME, + dropTipConfigurations: { plungerEject: { current: 1, speed: 15 } }, + liquids: { + default: { + $otSharedSchema: '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', + defaultTipracks: [ + 'opentrons/opentrons_flex_96_tiprack_1000ul/1', + 'opentrons/opentrons_flex_96_tiprack_200ul/1', + 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_1000ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_200ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + ], + minVolume: 5, + maxVolume: 1000, + supportedTips: expect.anything(), + }, + }, + model: 'p1000', + nozzleMap: expect.anything(), + pathTo3D: + 'pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf', + validNozzleMaps: { + maps: { + SingleA1: ['A1'], + }, + }, + pickUpTipConfigurations: { + pressFit: { + presses: 1, + increment: 0, + configurationsByNozzleMap: { + SingleA1: { + default: { + speed: 10, + distance: 13, + current: 0.2, + tipOverlaps: { + v0: { + default: 10.5, + 'opentrons/opentrons_flex_96_tiprack_1000ul/1': 9.65, + 'opentrons/opentrons_flex_96_tiprack_200ul/1': 9.76, + 'opentrons/opentrons_flex_96_tiprack_50ul/1': 10.09, + 'opentrons/opentrons_flex_96_filtertiprack_1000ul/1': 9.65, + 'opentrons/opentrons_flex_96_filtertiprack_200ul/1': 9.76, + 'opentrons/opentrons_flex_96_filtertiprack_50ul/1': 10.09, + }, + }, + }, + }, + }, + }, + }, + partialTipConfigurations: { + availableConfigurations: null, + partialTipSupported: false, + }, + plungerHomingConfigurations: { current: 1, speed: 30 }, + plungerMotorConfigurations: { idle: 0.3, run: 1 }, + plungerPositionsConfigurations: { + default: { blowout: 76.5, bottom: 71.5, drop: 90.5, top: 0 }, + }, + quirks: [], + shaftDiameter: 4.5, + shaftULperMM: 15.904, + nozzleOffset: [-8, -22, -259.15], + orderedColumns: expect.anything(), + orderedRows: expect.anything(), + pipetteBoundingBoxOffsets: { + backLeftCorner: [-8, -22, -259.15], + frontRightCorner: [-8, -22, -259.15], + }, + lldSettings: { + t50: { + minHeight: 1.0, + minVolume: 0, + }, + t200: { + minHeight: 1.0, + minVolume: 0, + }, + t1000: { + minHeight: 1.5, + minVolume: 0, + }, + }, +} as PipetteV2Specs + +describe('pipette data accessor hooks', () => { + beforeEach(() => { + vi.mocked(useIsOEMMode).mockReturnValue(false) + }) + + describe('usePipetteNameSpecs', () => { + it('returns the branded display name for P1000 single flex', () => { + expect(usePipetteNameSpecs('p1000_single_flex')?.displayName).toEqual( + BRANDED_P1000_FLEX_DISPLAY_NAME + ) + }) + + it('returns an anonymized display name in OEM mode', () => { + vi.mocked(useIsOEMMode).mockReturnValue(true) + expect(usePipetteNameSpecs('p1000_single_flex')?.displayName).toEqual( + ANONYMOUS_P1000_FLEX_DISPLAY_NAME + ) + }) + }) + + describe('usePipetteModelSpecs', () => { + it('returns the branded display name for P1000 single flex', () => { + expect(usePipetteModelSpecs('p1000_single_v3.6')?.displayName).toEqual( + BRANDED_P1000_FLEX_DISPLAY_NAME + ) + }) + + it('returns an anonymized display name in OEM mode', () => { + vi.mocked(useIsOEMMode).mockReturnValue(true) + expect(usePipetteModelSpecs('p1000_single_v3.6')?.displayName).toEqual( + ANONYMOUS_P1000_FLEX_DISPLAY_NAME + ) + }) + }) + + describe('usePipetteSpecsV2', () => { + it('returns the correct info for p1000_single_flex which should be the latest model version 3.7', () => { + expect(usePipetteSpecsV2('p1000_single_flex')).toStrictEqual( + mockP1000V2Specs + ) + }) + it('returns an anonymized display name in OEM mode', () => { + vi.mocked(useIsOEMMode).mockReturnValue(true) + expect(usePipetteSpecsV2('p1000_single_flex')).toStrictEqual({ + ...mockP1000V2Specs, + displayName: ANONYMOUS_P1000_FLEX_DISPLAY_NAME, + }) + }) + }) + + describe('useGripperDisplayName', () => { + it('returns the branded gripper display name', () => { + expect(useGripperDisplayName('gripperV1.3')).toEqual('Flex Gripper') + }) + it('returns an anonymized display name in OEM mode', () => { + vi.mocked(useIsOEMMode).mockReturnValue(true) + expect(useGripperDisplayName('gripperV1.3')).toEqual('Gripper') + }) + }) +}) diff --git a/app/src/resources/maintenance_runs/hooks/index.ts b/app/src/resources/maintenance_runs/hooks/index.ts new file mode 100644 index 00000000000..7d0230c9a0c --- /dev/null +++ b/app/src/resources/maintenance_runs/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useChainMaintenanceCommands' +export * from './useRobotControlCommands' diff --git a/app/src/resources/maintenance_runs/hooks/useChainMaintenanceCommands.ts b/app/src/resources/maintenance_runs/hooks/useChainMaintenanceCommands.ts new file mode 100644 index 00000000000..2f3a6e82434 --- /dev/null +++ b/app/src/resources/maintenance_runs/hooks/useChainMaintenanceCommands.ts @@ -0,0 +1,34 @@ +import * as React from 'react' + +import { useCreateMaintenanceCommandMutation } from '@opentrons/react-api-client' + +import { chainMaintenanceCommandsRecursive } from '../../runs' + +import type { CreateCommand } from '@opentrons/shared-data' + +export function useChainMaintenanceCommands(): { + chainRunCommands: ( + maintenanceRunId: string, + commands: CreateCommand[], + continuePastCommandFailure: boolean + ) => ReturnType + isCommandMutationLoading: boolean +} { + const [isLoading, setIsLoading] = React.useState(false) + const { createMaintenanceCommand } = useCreateMaintenanceCommandMutation() + return { + chainRunCommands: ( + maintenanceRunId, + commands: CreateCommand[], + continuePastCommandFailure: boolean + ) => + chainMaintenanceCommandsRecursive( + maintenanceRunId, + commands, + createMaintenanceCommand, + continuePastCommandFailure, + setIsLoading + ), + isCommandMutationLoading: isLoading, + } +} diff --git a/app/src/resources/maintenance_runs/hooks/useRobotControlCommands.ts b/app/src/resources/maintenance_runs/hooks/useRobotControlCommands.ts new file mode 100644 index 00000000000..427fff40183 --- /dev/null +++ b/app/src/resources/maintenance_runs/hooks/useRobotControlCommands.ts @@ -0,0 +1,110 @@ +import * as React from 'react' + +import { useDeleteMaintenanceRunMutation } from '@opentrons/react-api-client' + +import { useChainMaintenanceCommands } from './useChainMaintenanceCommands' +import { useCreateTargetedMaintenanceRunMutation } from '../../runs' + +import type { CreateCommand } from '@opentrons/shared-data' +import type { MaintenanceRun, Mount } from '@opentrons/api-client' + +export interface PipetteDetails { + mount: Mount + pipetteId: string + pipetteName?: string +} + +export interface UseRobotControlCommandsResult { + /* Creates the maintenance run, executes the commands utilizing the maintenance run context, then deletes the maintenance run. */ + executeCommands: () => Promise + /** + * Whether executeCommands is currently executing. This becomes "true" as the maintenance run is created and only + * becomes "false" after the maintenance run is deleted. + */ + isExecuting: boolean +} + +export interface UseRobotControlCommandsProps { + pipetteInfo: PipetteDetails | null + commands: CreateCommand[] + continuePastCommandFailure: boolean + /* An onSettled callback executed after the deletion of the maintenance run. */ + onSettled?: () => void +} +// Issue commands to the robot, creating an on-the-fly maintenance run for the duration of the issued commands, loading +// the relevant pipette if necessary. Commands are then executed, and regardless of the success status of those commands, +// the maintenance run is subsequently deleted. +export function useRobotControlCommands({ + pipetteInfo, + commands, + continuePastCommandFailure, + onSettled, +}: UseRobotControlCommandsProps): UseRobotControlCommandsResult { + const [isExecuting, setIsExecuting] = React.useState(false) + + const { chainRunCommands } = useChainMaintenanceCommands() + const { + mutateAsync: deleteMaintenanceRun, + } = useDeleteMaintenanceRunMutation() + + const { + createTargetedMaintenanceRun, + } = useCreateTargetedMaintenanceRunMutation({ + onSuccess: response => { + const runId = response.data.id as string + + const loadPipetteIfSupplied = (): Promise => { + if (pipetteInfo !== null) { + const loadPipetteCommand = buildLoadPipetteCommand(pipetteInfo) + return chainRunCommands(runId, [loadPipetteCommand], false) + .then(() => Promise.resolve()) + .catch((error: Error) => { + console.error(error.message) + }) + } + return Promise.resolve() + } + + // Execute the command(s) + loadPipetteIfSupplied() + .then(() => + chainRunCommands(runId, commands, continuePastCommandFailure) + ) + .catch((error: Error) => { + console.error(error.message) + }) + .finally(() => + deleteMaintenanceRun(runId).catch((error: Error) => { + console.error('Failed to delete maintenance run:', error.message) + }) + ) + .finally(() => { + onSettled?.() + setIsExecuting(false) + }) + }, + onError: (error: Error) => { + console.error(error.message) + setIsExecuting(false) + }, + }) + + const executeCommands = (): Promise => { + setIsExecuting(true) + return createTargetedMaintenanceRun({}) + } + + return { executeCommands, isExecuting } +} + +const buildLoadPipetteCommand = ( + pipetteDetails: PipetteDetails +): CreateCommand => { + return { + commandType: 'loadPipette', + params: { + ...pipetteDetails, + pipetteName: pipetteDetails.pipetteName ?? 'managedPipetteId', + }, + } +} diff --git a/app/src/resources/maintenance_runs/index.ts b/app/src/resources/maintenance_runs/index.ts index ecd7a95a94d..23d730f2402 100644 --- a/app/src/resources/maintenance_runs/index.ts +++ b/app/src/resources/maintenance_runs/index.ts @@ -1 +1,2 @@ -export * from './useNotifyCurrentMaintenanceRun' +export * from './notifications' +export * from './hooks' diff --git a/app/src/resources/maintenance_runs/notifications/index.ts b/app/src/resources/maintenance_runs/notifications/index.ts new file mode 100644 index 00000000000..ecd7a95a94d --- /dev/null +++ b/app/src/resources/maintenance_runs/notifications/index.ts @@ -0,0 +1 @@ +export * from './useNotifyCurrentMaintenanceRun' diff --git a/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts b/app/src/resources/maintenance_runs/notifications/useNotifyCurrentMaintenanceRun.ts similarity index 56% rename from app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts rename to app/src/resources/maintenance_runs/notifications/useNotifyCurrentMaintenanceRun.ts index 1d19084ba59..2d43d7b21c5 100644 --- a/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts +++ b/app/src/resources/maintenance_runs/notifications/useNotifyCurrentMaintenanceRun.ts @@ -1,24 +1,24 @@ import { useCurrentMaintenanceRun } from '@opentrons/react-api-client' -import { useNotifyDataReady } from '../useNotifyDataReady' +import { useNotifyDataReady } from '../../useNotifyDataReady' import type { UseQueryResult } from 'react-query' import type { MaintenanceRun } from '@opentrons/api-client' -import type { QueryOptionsWithPolling } from '../useNotifyDataReady' +import type { QueryOptionsWithPolling } from '../../useNotifyDataReady' export function useNotifyCurrentMaintenanceRun( options: QueryOptionsWithPolling = {} ): UseQueryResult | UseQueryResult { - const { notifyOnSettled, shouldRefetch } = useNotifyDataReady({ + const { shouldRefetch, queryOptionsNotify } = useNotifyDataReady({ topic: 'robot-server/maintenance_runs/current_run', options, }) - const httpQueryResult = useCurrentMaintenanceRun({ - ...options, - enabled: options?.enabled !== false && shouldRefetch, - onSettled: notifyOnSettled, - }) + const httpQueryResult = useCurrentMaintenanceRun(queryOptionsNotify) + + if (shouldRefetch) { + void httpQueryResult.refetch() + } return httpQueryResult } diff --git a/app/src/resources/runs/hooks.ts b/app/src/resources/runs/hooks.ts index 5b41edd6d4c..ffbf86abe28 100644 --- a/app/src/resources/runs/hooks.ts +++ b/app/src/resources/runs/hooks.ts @@ -4,27 +4,26 @@ import { useSelector } from 'react-redux' import { useCreateCommandMutation, useCreateLiveCommandMutation, - useCreateMaintenanceCommandMutation, useCreateMaintenanceRunMutation, } from '@opentrons/react-api-client' import { - chainRunCommandsRecursive, - chainMaintenanceCommandsRecursive, chainLiveCommandsRecursive, + chainRunCommandsRecursive, setCommandIntent, } from './utils' import { getIsOnDevice } from '../../redux/config' import { useMaintenanceRunTakeover } from '../../organisms/TakeoverModal' +import type { CreateCommand } from '@opentrons/shared-data' +import type { HostConfig } from '@opentrons/api-client' +import type { ModulePrepCommandsType } from '../../organisms/Devices/getModulePrepCommands' import type { + CreateMaintenanceRunType, UseCreateMaintenanceRunMutationOptions, UseCreateMaintenanceRunMutationResult, - CreateMaintenanceRunType, + useCreateMaintenanceCommandMutation, } from '@opentrons/react-api-client' -import type { CreateCommand } from '@opentrons/shared-data' -import type { HostConfig } from '@opentrons/api-client' -import type { ModulePrepCommandsType } from '../../organisms/Devices/getModulePrepCommands' export type CreateCommandMutate = ReturnType< typeof useCreateCommandMutation @@ -94,33 +93,6 @@ export function useChainRunCommands( } } -export function useChainMaintenanceCommands(): { - chainRunCommands: ( - maintenanceRunId: string, - commands: CreateCommand[], - continuePastCommandFailure: boolean - ) => ReturnType - isCommandMutationLoading: boolean -} { - const [isLoading, setIsLoading] = React.useState(false) - const { createMaintenanceCommand } = useCreateMaintenanceCommandMutation() - return { - chainRunCommands: ( - maintenanceRunId, - commands: CreateCommand[], - continuePastCommandFailure: boolean - ) => - chainMaintenanceCommandsRecursive( - maintenanceRunId, - commands, - createMaintenanceCommand, - continuePastCommandFailure, - setIsLoading - ), - isCommandMutationLoading: isLoading, - } -} - export function useChainLiveCommands(): { chainLiveCommands: ( commands: ModulePrepCommandsType[], diff --git a/app/src/resources/runs/index.ts b/app/src/resources/runs/index.ts index b9023f3f702..fa5d6a4fb02 100644 --- a/app/src/resources/runs/index.ts +++ b/app/src/resources/runs/index.ts @@ -5,3 +5,4 @@ export * from './useNotifyRunQuery' export * from './useNotifyAllCommandsQuery' export * from './useNotifyAllCommandsAsPreSerializedList' export * from './useCurrentRunId' +export * from './useIsRunCurrent' diff --git a/app/src/resources/runs/useIsRunCurrent.ts b/app/src/resources/runs/useIsRunCurrent.ts new file mode 100644 index 00000000000..7b9fabf6e86 --- /dev/null +++ b/app/src/resources/runs/useIsRunCurrent.ts @@ -0,0 +1,10 @@ +import { useNotifyRunQuery } from './useNotifyRunQuery' + +const CURRENT_RUN_POLL_MS = 5000 + +export function useIsRunCurrent(runId: string | null): boolean { + return Boolean( + useNotifyRunQuery(runId, { refetchInterval: CURRENT_RUN_POLL_MS })?.data + ?.data?.current + ) +} diff --git a/app/src/resources/runs/useNotifyAllCommandsAsPreSerializedList.ts b/app/src/resources/runs/useNotifyAllCommandsAsPreSerializedList.ts index 25d45392185..5668b824667 100644 --- a/app/src/resources/runs/useNotifyAllCommandsAsPreSerializedList.ts +++ b/app/src/resources/runs/useNotifyAllCommandsAsPreSerializedList.ts @@ -4,24 +4,28 @@ import { useNotifyDataReady } from '../useNotifyDataReady' import type { UseQueryResult } from 'react-query' import type { AxiosError } from 'axios' -import type { CommandsData, GetCommandsParams } from '@opentrons/api-client' +import type { CommandsData, GetRunCommandsParams } from '@opentrons/api-client' import type { QueryOptionsWithPolling } from '../useNotifyDataReady' export function useNotifyAllCommandsAsPreSerializedList( runId: string | null, - params?: GetCommandsParams | null, + params?: GetRunCommandsParams | null, options: QueryOptionsWithPolling = {} ): UseQueryResult { - const { notifyOnSettled, shouldRefetch } = useNotifyDataReady({ + const { shouldRefetch, queryOptionsNotify } = useNotifyDataReady({ topic: `robot-server/runs/pre_serialized_commands/${runId}`, options, }) - const httpResponse = useAllCommandsAsPreSerializedList(runId, params, { - ...options, - enabled: options?.enabled !== false && shouldRefetch, - onSettled: notifyOnSettled, - }) + const httpQueryResult = useAllCommandsAsPreSerializedList( + runId, + params, + queryOptionsNotify + ) + + if (shouldRefetch) { + void httpQueryResult.refetch() + } - return httpResponse + return httpQueryResult } diff --git a/app/src/resources/runs/useNotifyAllCommandsQuery.ts b/app/src/resources/runs/useNotifyAllCommandsQuery.ts index ec6fc65a38f..12bafb21ef3 100644 --- a/app/src/resources/runs/useNotifyAllCommandsQuery.ts +++ b/app/src/resources/runs/useNotifyAllCommandsQuery.ts @@ -3,12 +3,23 @@ import { useAllCommandsQuery } from '@opentrons/react-api-client' import { useNotifyDataReady } from '../useNotifyDataReady' import type { UseQueryResult } from 'react-query' -import type { CommandsData, GetCommandsParams } from '@opentrons/api-client' +import type { + CommandsData, + GetRunCommandsParams, + GetCommandsParams, +} from '@opentrons/api-client' import type { QueryOptionsWithPolling } from '../useNotifyDataReady' +const DEFAULT_PAGE_LENGTH = 30 + +export const DEFAULT_PARAMS: GetCommandsParams = { + cursor: null, + pageLength: DEFAULT_PAGE_LENGTH, +} + export function useNotifyAllCommandsQuery( runId: string | null, - params?: GetCommandsParams | null, + params?: GetRunCommandsParams | null, options: QueryOptionsWithPolling = {} ): UseQueryResult { // Assume the useAllCommandsQuery() response can only change when the command links change. @@ -17,16 +28,27 @@ export function useNotifyAllCommandsQuery( // running to succeeded, that may change the useAllCommandsQuery response, but it // will not necessarily change the command links. We might need an MQTT topic // covering "any change in `GET /runs/{id}/commands`". - const { notifyOnSettled, shouldRefetch } = useNotifyDataReady({ + const { shouldRefetch, queryOptionsNotify } = useNotifyDataReady({ topic: 'robot-server/runs/commands_links', options, }) + const nullCheckedParams = params ?? DEFAULT_PARAMS - const httpResponse = useAllCommandsQuery(runId, params, { - ...options, - enabled: options?.enabled !== false && shouldRefetch, - onSettled: notifyOnSettled, - }) + const nullCheckedFixitCommands = params?.includeFixitCommands ?? null + const finalizedNullCheckParams = { + ...nullCheckedParams, + includeFixitCommands: nullCheckedFixitCommands, + } + + const httpQueryResult = useAllCommandsQuery( + runId, + finalizedNullCheckParams, + queryOptionsNotify + ) + + if (shouldRefetch) { + void httpQueryResult.refetch() + } - return httpResponse + return httpQueryResult } diff --git a/app/src/resources/runs/useNotifyAllRunsQuery.ts b/app/src/resources/runs/useNotifyAllRunsQuery.ts index 68a8347c551..91633bbdc48 100644 --- a/app/src/resources/runs/useNotifyAllRunsQuery.ts +++ b/app/src/resources/runs/useNotifyAllRunsQuery.ts @@ -8,26 +8,27 @@ import type { HostConfig, GetRunsParams, Runs } from '@opentrons/api-client' import type { UseAllRunsQueryOptions } from '@opentrons/react-api-client/src/runs/useAllRunsQuery' import type { QueryOptionsWithPolling } from '../useNotifyDataReady' +// TODO(jh, 08-21-24): Abstract harder. export function useNotifyAllRunsQuery( params: GetRunsParams = {}, options: QueryOptionsWithPolling = {}, hostOverride?: HostConfig | null ): UseQueryResult { - const { notifyOnSettled, shouldRefetch } = useNotifyDataReady({ + const { shouldRefetch, queryOptionsNotify } = useNotifyDataReady({ topic: 'robot-server/runs', options, hostOverride, }) - const httpResponse = useAllRunsQuery( + const httpQueryResult = useAllRunsQuery( params, - { - ...(options as UseAllRunsQueryOptions), - enabled: options?.enabled !== false && shouldRefetch, - onSettled: notifyOnSettled, - }, + queryOptionsNotify as UseAllRunsQueryOptions, hostOverride ) - return httpResponse + if (shouldRefetch) { + void httpQueryResult.refetch() + } + + return httpQueryResult } diff --git a/app/src/resources/runs/useNotifyRunQuery.ts b/app/src/resources/runs/useNotifyRunQuery.ts index 003cfeabf94..1faf89e5c3f 100644 --- a/app/src/resources/runs/useNotifyRunQuery.ts +++ b/app/src/resources/runs/useNotifyRunQuery.ts @@ -12,23 +12,17 @@ export function useNotifyRunQuery( options: QueryOptionsWithPolling = {}, hostOverride?: HostConfig | null ): UseQueryResult { - const isEnabled = options.enabled !== false && runId != null - - const { notifyOnSettled, shouldRefetch } = useNotifyDataReady({ + const { shouldRefetch, queryOptionsNotify } = useNotifyDataReady({ topic: `robot-server/runs/${runId}` as NotifyTopic, - options: { ...options, enabled: options.enabled != null && runId != null }, + options, hostOverride, }) - const httpResponse = useRunQuery( - runId, - { - ...options, - enabled: isEnabled && shouldRefetch, - onSettled: notifyOnSettled, - }, - hostOverride - ) + const httpQueryResult = useRunQuery(runId, queryOptionsNotify, hostOverride) + + if (shouldRefetch) { + void httpQueryResult.refetch() + } - return httpResponse + return httpQueryResult } diff --git a/app/src/resources/useNotifyDataReady.ts b/app/src/resources/useNotifyDataReady.ts index f4c485f4ff2..dfc0e066daf 100644 --- a/app/src/resources/useNotifyDataReady.ts +++ b/app/src/resources/useNotifyDataReady.ts @@ -16,29 +16,40 @@ import type { UseQueryOptions } from 'react-query' import type { HostConfig } from '@opentrons/api-client' import type { NotifyTopic, NotifyResponseData } from '../redux/shell/types' -export type HTTPRefetchFrequency = 'once' | 'always' | null +export type HTTPRefetchFrequency = 'once' | null export interface QueryOptionsWithPolling extends UseQueryOptions { forceHttpPolling?: boolean } -interface useNotifyDataReadyProps { +interface UseNotifyDataReadyProps { topic: NotifyTopic options: QueryOptionsWithPolling hostOverride?: HostConfig | null } -interface useNotifyDataReadyResults { - notifyOnSettled: () => void +interface UseNotifyDataReadyResults { + /* React Query options with notification-specific logic. */ + queryOptionsNotify: QueryOptionsWithPolling + /* Whether notifications indicate the server has new data ready. Always returns false if notifications are disabled. */ shouldRefetch: boolean } +// React query hooks perform refetches when instructed by the shell via a refetch mechanism, which useNotifyDataReady manages. +// The notification refetch states may be: +// 'once' - The shell has received an MQTT update. Execute the HTTP refetch once. +// null - The shell has not received an MQTT update. Don't execute an HTTP refetch. +// +// Eagerly assume notifications are enabled unless specified by the client via React Query options or by the shell via errors. export function useNotifyDataReady({ topic, options, hostOverride, -}: useNotifyDataReadyProps): useNotifyDataReadyResults { +}: UseNotifyDataReadyProps): UseNotifyDataReadyResults< + TData, + TError +> { const dispatch = useDispatch() const hostFromProvider = useHost() const host = hostOverride ?? hostFromProvider @@ -47,6 +58,7 @@ export function useNotifyDataReady({ const forcePollingFF = useFeatureFlag('forceHttpPolling') const seenHostname = React.useRef(null) const [refetch, setRefetch] = React.useState(null) + const [isNotifyEnabled, setIsNotifyEnabled] = React.useState(true) const { enabled, staleTime, forceHttpPolling } = options @@ -69,7 +81,7 @@ export function useNotifyDataReady({ dispatch(notifySubscribeAction(hostname, topic)) seenHostname.current = hostname } else { - setRefetch('always') + setIsNotifyEnabled(false) } return () => { @@ -86,7 +98,7 @@ export function useNotifyDataReady({ const onDataEvent = React.useCallback((data: NotifyResponseData): void => { if (data === 'ECONNFAILED' || data === 'ECONNREFUSED') { - setRefetch('always') + setIsNotifyEnabled(false) if (data === 'ECONNREFUSED') { doTrackEvent({ name: ANALYTICS_NOTIFICATION_PORT_BLOCK_ERROR, @@ -98,11 +110,24 @@ export function useNotifyDataReady({ } }, []) - const notifyOnSettled = React.useCallback(() => { - if (refetch === 'once') { - setRefetch(null) - } - }, [refetch]) - - return { notifyOnSettled, shouldRefetch: refetch != null } + const notifyOnSettled = React.useCallback( + (data: TData | undefined, error: TError | null) => { + if (refetch === 'once') { + setRefetch(null) + } + options.onSettled?.(data, error) + }, + [refetch, options.onSettled] + ) + + const queryOptionsNotify = { + ...options, + onSettled: isNotifyEnabled ? notifyOnSettled : options.onSettled, + refetchInterval: isNotifyEnabled ? false : options.refetchInterval, + } + + return { + queryOptionsNotify, + shouldRefetch: isNotifyEnabled && refetch != null, + } } diff --git a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx b/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx similarity index 72% rename from app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx rename to app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx index 95cc478f7b6..303ea03a784 100644 --- a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx +++ b/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx @@ -1,8 +1,8 @@ +import omitBy from 'lodash/omitBy' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' +import type { UseQueryResult } from 'react-query' import { renderHook } from '@testing-library/react' -import { when } from 'vitest-when' -import omitBy from 'lodash/omitBy' - +import type { Protocol } from '@opentrons/api-client' import { useProtocolQuery, useProtocolAnalysisAsDocumentQuery, @@ -14,30 +14,20 @@ import { WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, fixtureTiprack300ul, } from '@opentrons/shared-data' -import { - useMissingProtocolHardware, - useRequiredProtocolLabware, - useRunTimeParameters, -} from '../index' -import { useNotifyDeckConfigurationQuery } from '../../../../resources/deck_configuration/useNotifyDeckConfigurationQuery' -import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' - -import type { UseQueryResult } from 'react-query' import type { CompletedProtocolAnalysis, DeckConfiguration, LabwareDefinition2, } from '@opentrons/shared-data' -import type { Protocol } from '@opentrons/api-client' +import { useNotifyDeckConfigurationQuery } from '../../../../resources/deck_configuration/useNotifyDeckConfigurationQuery' +import { useMissingProtocolHardware } from '../useMissingProtocolHardware' +import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' vi.mock('@opentrons/react-api-client') -vi.mock('../../../../organisms/Devices/hooks') -vi.mock('../../../../redux/config') vi.mock( '../../../../resources/deck_configuration/useNotifyDeckConfigurationQuery' ) -const PROTOCOL_ID = 'fake_protocol_id' const mockRTPData = [ { displayName: 'Dry Run', @@ -172,100 +162,6 @@ const PROTOCOL_ANALYSIS = { ], runTimeParameters: mockRTPData, } as any - -const NULL_COMMAND = { - id: '97ba49a5-04f6-4f91-986a-04a0eb632882', - createdAt: '2022-09-07T19:47:42.781065+00:00', - commandType: 'loadPipette', - key: '0feeecaf-3895-46d7-ab71-564601265e35', - status: 'succeeded', - params: { - pipetteName: 'p20_single_gen2', - mount: 'left', - pipetteId: '90183a18-a1df-4fd6-9636-be3bcec63fe4', - }, - result: { - pipetteId: '90183a18-a1df-4fd6-9636-be3bcec63fe4', - }, - startedAt: '2022-09-07T19:47:42.782665+00:00', - completedAt: '2022-09-07T19:47:42.785061+00:00', -} -const NULL_PROTOCOL_ANALYSIS = { - ...PROTOCOL_ANALYSIS, - id: 'null_analysis', - commands: [NULL_COMMAND], -} as any - -describe('useRunTimeParameters', () => { - beforeEach(() => { - when(vi.mocked(useProtocolQuery)) - .calledWith(PROTOCOL_ID) - .thenReturn({ - data: { - data: { analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id } as any] }, - }, - } as UseQueryResult) - when(vi.mocked(useProtocolAnalysisAsDocumentQuery)) - .calledWith(PROTOCOL_ID, PROTOCOL_ANALYSIS.id, { enabled: true }) - .thenReturn({ - data: PROTOCOL_ANALYSIS, - } as UseQueryResult) - }) - it('return RTP', () => { - const { result } = renderHook(() => useRunTimeParameters(PROTOCOL_ID)) - expect(result.current).toBe(mockRTPData) - }) -}) -describe('useRequiredProtocolLabware', () => { - beforeEach(() => { - when(vi.mocked(useProtocolQuery)) - .calledWith(PROTOCOL_ID) - .thenReturn({ - data: { - data: { analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id } as any] }, - }, - } as UseQueryResult) - when(vi.mocked(useProtocolAnalysisAsDocumentQuery)) - .calledWith(PROTOCOL_ID, PROTOCOL_ANALYSIS.id, { enabled: true }) - .thenReturn({ - data: PROTOCOL_ANALYSIS, - } as UseQueryResult) - when(vi.mocked(useProtocolAnalysisAsDocumentQuery)) - .calledWith(PROTOCOL_ID, NULL_PROTOCOL_ANALYSIS.id, { enabled: true }) - .thenReturn({ - data: NULL_PROTOCOL_ANALYSIS, - } as UseQueryResult) - }) - - afterEach(() => { - vi.resetAllMocks() - }) - - it('should return LabwareSetupItem array', () => { - const { result } = renderHook(() => useRequiredProtocolLabware(PROTOCOL_ID)) - expect(result.current.length).toBe(1) - expect(result.current[0].nickName).toEqual('first labware nickname') - expect(result.current[0].definition.dimensions.xDimension).toBe(127.76) - expect(result.current[0].definition.metadata.displayName).toEqual( - '300ul Tiprack FIXTURE' - ) - }) - - it('should return empty array when there is no match with protocol id', () => { - when(vi.mocked(useProtocolQuery)) - .calledWith(PROTOCOL_ID) - .thenReturn({ - data: { - data: { - analysisSummaries: [{ id: NULL_PROTOCOL_ANALYSIS.id } as any], - }, - }, - } as UseQueryResult) - const { result } = renderHook(() => useRequiredProtocolLabware(PROTOCOL_ID)) - expect(result.current.length).toBe(0) - }) -}) - describe.only('useMissingProtocolHardware', () => { let wrapper: React.FunctionComponent<{ children: React.ReactNode }> beforeEach(() => { diff --git a/app/src/transformations/commands/hooks/index.ts b/app/src/transformations/commands/hooks/index.ts new file mode 100644 index 00000000000..4765bb03363 --- /dev/null +++ b/app/src/transformations/commands/hooks/index.ts @@ -0,0 +1,5 @@ +export * from './useMissingProtocolHardware' +export * from './useMissingProtocolHardwareFromRequiredProtocolHardware' +export * from './useRequiredProtocolHardwareFromAnalysis' +export * from './useMissingProtocolHardwareFromAnalysis' +export * from './types' diff --git a/app/src/transformations/commands/hooks/types.ts b/app/src/transformations/commands/hooks/types.ts new file mode 100644 index 00000000000..c2a37e639c9 --- /dev/null +++ b/app/src/transformations/commands/hooks/types.ts @@ -0,0 +1,39 @@ +import type { + CutoutFixtureId, + CutoutId, + ModuleModel, + PipetteName, +} from '@opentrons/shared-data' + +export interface ProtocolPipette { + hardwareType: 'pipette' + pipetteName: PipetteName + mount: 'left' | 'right' + connected: boolean +} + +export interface ProtocolModule { + hardwareType: 'module' + moduleModel: ModuleModel + slot: string + connected: boolean + hasSlotConflict: boolean +} + +export interface ProtocolGripper { + hardwareType: 'gripper' + connected: boolean +} + +export interface ProtocolFixture { + hardwareType: 'fixture' + cutoutFixtureId: CutoutFixtureId | null + location: { cutout: CutoutId } + hasSlotConflict: boolean +} + +export type ProtocolHardware = + | ProtocolPipette + | ProtocolModule + | ProtocolGripper + | ProtocolFixture diff --git a/app/src/transformations/commands/hooks/useMissingProtocolHardware.ts b/app/src/transformations/commands/hooks/useMissingProtocolHardware.ts new file mode 100644 index 00000000000..423a7d61e74 --- /dev/null +++ b/app/src/transformations/commands/hooks/useMissingProtocolHardware.ts @@ -0,0 +1,36 @@ +import last from 'lodash/last' +import { + useProtocolQuery, + useProtocolAnalysisAsDocumentQuery, +} from '@opentrons/react-api-client' + +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import type { ProtocolHardware } from './types' +import { useRequiredProtocolHardwareFromAnalysis } from './useRequiredProtocolHardwareFromAnalysis' +import { useMissingProtocolHardwareFromRequiredProtocolHardware } from './useMissingProtocolHardwareFromRequiredProtocolHardware' + +export const useMissingProtocolHardware = ( + protocolId: string +): { + missingProtocolHardware: ProtocolHardware[] + conflictedSlots: string[] + isLoading: boolean +} => { + const { data: protocolData } = useProtocolQuery(protocolId) + const { data: analysis } = useProtocolAnalysisAsDocumentQuery( + protocolId, + last(protocolData?.data.analysisSummaries)?.id ?? null, + { enabled: protocolData != null } + ) + const { + requiredProtocolHardware, + isLoading, + } = useRequiredProtocolHardwareFromAnalysis(analysis ?? null) + + return useMissingProtocolHardwareFromRequiredProtocolHardware( + requiredProtocolHardware, + isLoading, + FLEX_ROBOT_TYPE, + analysis ?? null + ) +} diff --git a/app/src/transformations/commands/hooks/useMissingProtocolHardwareFromAnalysis.ts b/app/src/transformations/commands/hooks/useMissingProtocolHardwareFromAnalysis.ts new file mode 100644 index 00000000000..33879c38370 --- /dev/null +++ b/app/src/transformations/commands/hooks/useMissingProtocolHardwareFromAnalysis.ts @@ -0,0 +1,28 @@ +import type { + RobotType, + CompletedProtocolAnalysis, +} from '@opentrons/shared-data' +import { useRequiredProtocolHardwareFromAnalysis } from './useRequiredProtocolHardwareFromAnalysis' +import { useMissingProtocolHardwareFromRequiredProtocolHardware } from './useMissingProtocolHardwareFromRequiredProtocolHardware' +import type { ProtocolHardware } from './types' + +export const useMissingProtocolHardwareFromAnalysis = ( + robotType: RobotType, + analysis: CompletedProtocolAnalysis | null +): { + missingProtocolHardware: ProtocolHardware[] + conflictedSlots: string[] + isLoading: boolean +} => { + const { + requiredProtocolHardware, + isLoading, + } = useRequiredProtocolHardwareFromAnalysis(analysis) + + return useMissingProtocolHardwareFromRequiredProtocolHardware( + requiredProtocolHardware, + isLoading, + robotType, + analysis ?? null + ) +} diff --git a/app/src/transformations/commands/hooks/useMissingProtocolHardwareFromRequiredProtocolHardware.ts b/app/src/transformations/commands/hooks/useMissingProtocolHardwareFromRequiredProtocolHardware.ts new file mode 100644 index 00000000000..5b94be3cc87 --- /dev/null +++ b/app/src/transformations/commands/hooks/useMissingProtocolHardwareFromRequiredProtocolHardware.ts @@ -0,0 +1,74 @@ +import { FLEX_MODULE_ADDRESSABLE_AREAS } from '@opentrons/shared-data' +import type { + CompletedProtocolAnalysis, + ProtocolAnalysisOutput, + RobotType, +} from '@opentrons/shared-data' +import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' +import type { ProtocolHardware, ProtocolModule, ProtocolFixture } from './types' + +/** + * Returns an array of ProtocolHardware objects that are required by the given protocol ID, + * but not currently connected. + * + * @param {ProtocolHardware[]} requiredProtocolHardware An array of ProtocolHardware objects that are required by a protocol. + * @param {boolean} isLoading A boolean determining whether any required protocol hardware is loading. + * @returns {ProtocolHardware[]} An array of ProtocolHardware objects that are required by the given protocol ID, but not currently connected. + */ + +export const useMissingProtocolHardwareFromRequiredProtocolHardware = ( + requiredProtocolHardware: ProtocolHardware[], + isLoading: boolean, + robotType: RobotType, + protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null +): { + missingProtocolHardware: ProtocolHardware[] + conflictedSlots: string[] + isLoading: boolean +} => { + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + protocolAnalysis + ) + // determine missing or conflicted hardware + return { + missingProtocolHardware: [ + ...requiredProtocolHardware.filter( + hardware => 'connected' in hardware && !hardware.connected + ), + ...deckConfigCompatibility + .filter( + ({ + cutoutFixtureId, + compatibleCutoutFixtureIds, + requiredAddressableAreas, + }) => + cutoutFixtureId != null && + !compatibleCutoutFixtureIds.some(id => id === cutoutFixtureId) && + !FLEX_MODULE_ADDRESSABLE_AREAS.some(modAA => + requiredAddressableAreas.includes(modAA) + ) // modules are already included via requiredProtocolHardware + ) + .map(({ compatibleCutoutFixtureIds, cutoutId }) => ({ + hardwareType: 'fixture' as const, + cutoutFixtureId: compatibleCutoutFixtureIds[0], + location: { cutout: cutoutId }, + hasSlotConflict: true, + })), + ], + conflictedSlots: requiredProtocolHardware + .filter( + (hardware): hardware is ProtocolModule | ProtocolFixture => + (hardware.hardwareType === 'module' || + hardware.hardwareType === 'fixture') && + hardware.hasSlotConflict + ) + .map( + hardware => + hardware.hardwareType === 'module' + ? hardware.slot // module + : hardware.location.cutout // fixture + ), + isLoading, + } +} diff --git a/app/src/transformations/commands/hooks/useRequiredProtocolHardwareFromAnalysis.ts b/app/src/transformations/commands/hooks/useRequiredProtocolHardwareFromAnalysis.ts new file mode 100644 index 00000000000..95b5886072c --- /dev/null +++ b/app/src/transformations/commands/hooks/useRequiredProtocolHardwareFromAnalysis.ts @@ -0,0 +1,165 @@ +import { + useInstrumentsQuery, + useModulesQuery, +} from '@opentrons/react-api-client' +import { + FLEX_ROBOT_TYPE, + getCutoutIdForSlotName, + getDeckDefFromRobotType, + getCutoutFixturesForModuleModel, + getCutoutFixtureIdsForModuleModel, + getModuleType, + FLEX_USB_MODULE_ADDRESSABLE_AREAS, + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, + MAGNETIC_BLOCK_TYPE, +} from '@opentrons/shared-data' +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' +import { + useNotifyDeckConfigurationQuery, + useDeckConfigurationCompatibility, +} from '../../../resources/deck_configuration' +import type { + ProtocolHardware, + ProtocolGripper, + ProtocolModule, + ProtocolPipette, +} from './types' +import { getProtocolUsesGripper } from '../transformations' + +const DECK_CONFIG_REFETCH_INTERVAL = 5000 + +export const useRequiredProtocolHardwareFromAnalysis = ( + analysis: CompletedProtocolAnalysis | null +): { requiredProtocolHardware: ProtocolHardware[]; isLoading: boolean } => { + const { + data: attachedModulesData, + isLoading: isLoadingModules, + } = useModulesQuery() + const attachedModules = attachedModulesData?.data ?? [] + + const { + data: attachedInstrumentsData, + isLoading: isLoadingInstruments, + } = useInstrumentsQuery() + const attachedInstruments = attachedInstrumentsData?.data ?? [] + + const robotType = FLEX_ROBOT_TYPE + const deckDef = getDeckDefFromRobotType(robotType) + const deckConfig = + useNotifyDeckConfigurationQuery({ + refetchInterval: DECK_CONFIG_REFETCH_INTERVAL, + })?.data ?? [] + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + analysis + ) + + if (analysis == null || analysis?.status !== 'completed') { + return { requiredProtocolHardware: [], isLoading: true } + } + + const requiredGripper: ProtocolGripper[] = getProtocolUsesGripper(analysis) + ? [ + { + hardwareType: 'gripper', + connected: + attachedInstruments.some(i => i.instrumentType === 'gripper') ?? + false, + }, + ] + : [] + + const requiredModules: ProtocolModule[] = analysis.modules + // remove magnetic blocks, they're handled by required fixtures + .filter(m => getModuleType(m.model) !== MAGNETIC_BLOCK_TYPE) + .map(({ location, model }) => { + const cutoutIdForSlotName = getCutoutIdForSlotName( + location.slotName, + deckDef + ) + const moduleFixtures = getCutoutFixturesForModuleModel(model, deckDef) + + const configuredModuleSerialNumber = + deckConfig.find( + ({ cutoutId, cutoutFixtureId }) => + cutoutId === cutoutIdForSlotName && + moduleFixtures.map(mf => mf.id).includes(cutoutFixtureId) + )?.opentronsModuleSerialNumber ?? null + const isConnected = moduleFixtures.every( + mf => mf.expectOpentronsModuleSerialNumber + ) + ? attachedModules.some( + m => + m.moduleModel === model && + m.serialNumber === configuredModuleSerialNumber + ) + : true + return { + hardwareType: 'module', + moduleModel: model, + slot: location.slotName, + connected: isConnected, + hasSlotConflict: deckConfig.some( + ({ cutoutId, cutoutFixtureId }) => + cutoutId === getCutoutIdForSlotName(location.slotName, deckDef) && + !getCutoutFixtureIdsForModuleModel(model).includes(cutoutFixtureId) + ), + } + }) + + const requiredPipettes: ProtocolPipette[] = analysis.pipettes.map( + ({ mount, pipetteName }) => ({ + hardwareType: 'pipette', + pipetteName: pipetteName, + mount: mount, + connected: + attachedInstruments.some( + i => + i.instrumentType === 'pipette' && + i.ok && + i.mount === mount && + i.instrumentName === pipetteName + ) ?? false, + }) + ) + + // fixture includes at least 1 required addressableArea AND it doesn't ONLY include a single slot addressableArea + const requiredDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => { + const atLeastOneAA = requiredAddressableAreas.length > 0 + const notOnlySingleSlot = !( + requiredAddressableAreas.length === 1 && + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(requiredAddressableAreas[0]) + ) + return atLeastOneAA && notOnlySingleSlot + } + ) + + const requiredFixtures = requiredDeckConfigCompatibility + // filter out all fixtures that only provide usb module addressable areas + // as they're handled in the requiredModules section via hardwareType === 'module' + .filter( + ({ requiredAddressableAreas }) => + !requiredAddressableAreas.every(modAA => + FLEX_USB_MODULE_ADDRESSABLE_AREAS.includes(modAA) + ) + ) + .map(({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ + hardwareType: 'fixture' as const, + cutoutFixtureId: compatibleCutoutFixtureIds[0], + location: { cutout: cutoutId }, + hasSlotConflict: + cutoutFixtureId != null && + !compatibleCutoutFixtureIds.includes(cutoutFixtureId), + })) + + return { + requiredProtocolHardware: [ + ...requiredPipettes, + ...requiredModules, + ...requiredGripper, + ...requiredFixtures, + ], + isLoading: isLoadingInstruments || isLoadingModules, + } +} diff --git a/app/src/transformations/commands/index.ts b/app/src/transformations/commands/index.ts new file mode 100644 index 00000000000..9a6ffc01152 --- /dev/null +++ b/app/src/transformations/commands/index.ts @@ -0,0 +1,2 @@ +export * from './hooks' +export * from './transformations' diff --git a/app/src/pages/Protocols/utils/index.ts b/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts similarity index 100% rename from app/src/pages/Protocols/utils/index.ts rename to app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts diff --git a/app/src/transformations/commands/transformations/getProtocolUsesGripper.ts b/app/src/transformations/commands/transformations/getProtocolUsesGripper.ts new file mode 100644 index 00000000000..d308e6c29a4 --- /dev/null +++ b/app/src/transformations/commands/transformations/getProtocolUsesGripper.ts @@ -0,0 +1,15 @@ +import type { + ProtocolAnalysisOutput, + CompletedProtocolAnalysis, +} from '@opentrons/shared-data' + +export function getProtocolUsesGripper( + analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput +): boolean { + return ( + analysis?.commands.some( + c => + c.commandType === 'moveLabware' && c.params.strategy === 'usingGripper' + ) ?? false + ) +} diff --git a/app/src/transformations/commands/transformations/index.ts b/app/src/transformations/commands/transformations/index.ts new file mode 100644 index 00000000000..47b64721bc9 --- /dev/null +++ b/app/src/transformations/commands/transformations/index.ts @@ -0,0 +1,2 @@ +export * from './getLabwareSetupItemGroups' +export * from './getProtocolUsesGripper' diff --git a/components/src/atoms/Chip/index.tsx b/components/src/atoms/Chip/index.tsx index 71e2a1b4510..f3bafa4187d 100644 --- a/components/src/atoms/Chip/index.tsx +++ b/components/src/atoms/Chip/index.tsx @@ -3,7 +3,7 @@ import { css } from 'styled-components' import { BORDERS, COLORS } from '../../helix-design-system' import { Flex } from '../../primitives' import { LegacyStyledText } from '../StyledText' -import { ALIGN_CENTER, DIRECTION_ROW } from '../../styles' +import { ALIGN_CENTER, DIRECTION_ROW, FLEX_MAX_CONTENT } from '../../styles' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { Icon } from '../../icons' @@ -91,6 +91,7 @@ export function Chip(props: ChipProps): JSX.Element { const MEDIUM_CONTAINER_STYLE = css` padding: ${SPACING.spacing2} ${background === false ? 0 : SPACING.spacing8}; grid-gap: ${SPACING.spacing4}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { padding: ${SPACING.spacing8} ${background === false ? 0 : SPACING.spacing16}; @@ -101,6 +102,7 @@ export function Chip(props: ChipProps): JSX.Element { const SMALL_CONTAINER_STYLE = css` padding: ${SPACING.spacing4} ${background === false ? 0 : SPACING.spacing6}; grid-gap: ${SPACING.spacing4}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { padding: ${SPACING.spacing4} ${background === false ? 0 : SPACING.spacing8}; @@ -111,6 +113,7 @@ export function Chip(props: ChipProps): JSX.Element { const ICON_STYLE = css` width: ${chipSize === 'medium' ? '1rem' : '0.75rem'}; height: ${chipSize === 'medium' ? '1rem' : '0.75rem'}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { width: ${chipSize === 'medium' ? '1.5rem' : '1.25rem'}; height: ${chipSize === 'medium' ? '1.5rem' : '1.25rem'}; @@ -119,6 +122,7 @@ export function Chip(props: ChipProps): JSX.Element { const TEXT_STYLE = css` ${chipSize === 'medium' ? WEB_MEDIUM_TEXT_STYLE : WEB_SMALL_TEXT_STYLE} + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { ${chipSize === 'medium' ? TYPOGRAPHY.bodyTextSemiBold @@ -132,6 +136,7 @@ export function Chip(props: ChipProps): JSX.Element { backgroundColor={backgroundColor} borderRadius={CHIP_PROPS_BY_TYPE[type].borderRadius} flexDirection={DIRECTION_ROW} + height={FLEX_MAX_CONTENT} css={ chipSize === 'medium' ? MEDIUM_CONTAINER_STYLE : SMALL_CONTAINER_STYLE } diff --git a/components/src/atoms/Divider/index.tsx b/components/src/atoms/Divider/index.tsx new file mode 100644 index 00000000000..b1e8c158722 --- /dev/null +++ b/components/src/atoms/Divider/index.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import { Box, COLORS, SPACING } from '../..' + +type Props = React.ComponentProps + +export function Divider(props: Props): JSX.Element { + return ( + + ) +} diff --git a/components/src/atoms/InputField/index.tsx b/components/src/atoms/InputField/index.tsx index e0155ffce4c..cd471996ef3 100644 --- a/components/src/atoms/InputField/index.tsx +++ b/components/src/atoms/InputField/index.tsx @@ -2,14 +2,14 @@ import * as React from 'react' import styled, { css } from 'styled-components' import { Flex } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' +import { ALIGN_CENTER, DIRECTION_COLUMN, TEXT_ALIGN_RIGHT } from '../../styles' import { BORDERS, COLORS } from '../../helix-design-system' import { Icon } from '../../icons' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { Tooltip } from '../Tooltip' import { useHoverTooltip } from '../../tooltips' import { LegacyStyledText } from '../StyledText' - +import type { IconName } from '../../icons' export const INPUT_TYPE_NUMBER = 'number' as const export const LEGACY_INPUT_TYPE_TEXT = 'text' as const export const LEGACY_INPUT_TYPE_PASSWORD = 'password' as const @@ -68,6 +68,9 @@ export interface InputFieldProps { size?: 'medium' | 'small' /** react useRef to control input field instead of react event */ ref?: React.MutableRefObject + leftIcon?: IconName + showDeleteIcon?: boolean + onDelete?: () => void } export const InputField = React.forwardRef( @@ -79,6 +82,7 @@ export const InputField = React.forwardRef( title, tooltipText, tabIndex = 0, + showDeleteIcon = false, ...inputProps } = props const hasError = props.error != null @@ -106,7 +110,7 @@ export const InputField = React.forwardRef( ${hasError ? COLORS.red50 : COLORS.grey50}; font-size: ${TYPOGRAPHY.fontSizeP}; width: 100%; - height: 2rem; + height: ${size === 'small' ? '2rem' : '2.75rem'}; &:active:enabled { border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; @@ -275,6 +279,15 @@ export const InputField = React.forwardRef( } }} > + {props.leftIcon != null ? ( + + + + ) : null} ( {props.units != null ? ( {props.units} ) : null} + {showDeleteIcon ? ( + + + + ) : null} {props.caption != null ? ( diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx index 7a51b2cee16..fd966ca0feb 100644 --- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx +++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx @@ -16,13 +16,7 @@ export function ListButtonAccordionContainer( const { id, children } = props return ( - { - e.stopPropagation() - }} - > + {children} ) diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx index 75161baa1a6..bbcc98e12d5 100644 --- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx +++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx @@ -11,6 +11,8 @@ interface ListButtonRadioButtonProps extends StyleProps { buttonText: string buttonValue: string | number onChange: React.ChangeEventHandler + setNoHover?: () => void + setHovered?: () => void disabled?: boolean isSelected?: boolean id?: string @@ -26,6 +28,8 @@ export function ListButtonRadioButton( isSelected = false, onChange, disabled = false, + setHovered, + setNoHover, id = buttonText, } = props @@ -67,7 +71,13 @@ export function ListButtonRadioButton( ` return ( - + { + e.stopPropagation() + }} + > - + {buttonText} diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx index 9a2482d3f00..b329b2fdc78 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { css } from 'styled-components' import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../../styles' import { COLORS } from '../../../helix-design-system' import { Flex, Link } from '../../../primitives' @@ -53,13 +54,20 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { {tag != null ? : null} {onClick != null && linkText != null ? ( - - - - {linkText} - - - + + {linkText} + ) : null} ) diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx index 45cf9bfceb1..92fc85182f6 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { Flex } from '../../../primitives' import { + ALIGN_CENTER, DIRECTION_ROW, FLEX_AUTO, JUSTIFY_SPACE_BETWEEN, @@ -22,6 +23,7 @@ export const ListItemDescriptor = ( flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing8} width="100%" + alignItems={ALIGN_CENTER} justifyContent={type === 'mini' ? JUSTIFY_SPACE_BETWEEN : 'none'} padding={ type === 'mini' @@ -29,8 +31,13 @@ export const ListItemDescriptor = ( : SPACING.spacing12 } > - {description} - {content} + + {description} + + {content} ) } diff --git a/components/src/atoms/ListItem/__tests__/ListItem.test.tsx b/components/src/atoms/ListItem/__tests__/ListItem.test.tsx index ed8ffcee095..0429b65da73 100644 --- a/components/src/atoms/ListItem/__tests__/ListItem.test.tsx +++ b/components/src/atoms/ListItem/__tests__/ListItem.test.tsx @@ -34,7 +34,7 @@ describe('ListItem', () => { render(props) screen.getByText('mock listitem content') const listItem = screen.getByTestId('ListItem_noActive') - expect(listItem).toHaveStyle(`backgroundColor: ${COLORS.grey35}`) + expect(listItem).toHaveStyle(`backgroundColor: ${COLORS.grey30}`) expect(listItem).toHaveStyle(`padding: 0`) expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius4}`) }) diff --git a/components/src/atoms/ListItem/index.tsx b/components/src/atoms/ListItem/index.tsx index 53555e7bea3..93ac9ca46e5 100644 --- a/components/src/atoms/ListItem/index.tsx +++ b/components/src/atoms/ListItem/index.tsx @@ -26,7 +26,7 @@ const LISTITEM_PROPS_BY_TYPE: Record< backgroundColor: COLORS.red35, }, noActive: { - backgroundColor: COLORS.grey35, + backgroundColor: COLORS.grey30, }, success: { backgroundColor: COLORS.green35, diff --git a/components/src/atoms/buttons/EmptySelectorButton.tsx b/components/src/atoms/buttons/EmptySelectorButton.tsx index 8c3adc765b0..2726c2045d3 100644 --- a/components/src/atoms/buttons/EmptySelectorButton.tsx +++ b/components/src/atoms/buttons/EmptySelectorButton.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import styled from 'styled-components' import { Flex } from '../../primitives' import { BORDERS, @@ -6,7 +7,6 @@ import { Icon, SPACING, StyledText, - Btn, JUSTIFY_CENTER, JUSTIFY_START, ALIGN_CENTER, @@ -20,23 +20,43 @@ interface EmptySelectorButtonProps { textAlignment: 'left' | 'middle' iconName?: IconName size?: 'large' | 'small' + disabled?: boolean } // used for helix and Opentrons Ai export function EmptySelectorButton( props: EmptySelectorButtonProps ): JSX.Element { - const { onClick, text, iconName, size = 'large', textAlignment } = props + const { + onClick, + text, + iconName, + size = 'large', + textAlignment, + disabled = false, + } = props const buttonSizing = size === 'large' ? '100%' : FLEX_MAX_CONTENT + const StyledButton = styled.button` + border: none; + width: ${buttonSizing}; + height: ${buttonSizing}; + &:focus-visible { + outline: 2px solid ${COLORS.white}; + box-shadow: 0 0 0 4px ${COLORS.blue50}; + border-radius: ${BORDERS.borderRadius8}; + } + ` + return ( - + {text} - + ) } diff --git a/components/src/atoms/buttons/LargeButton.stories.tsx b/components/src/atoms/buttons/LargeButton.stories.tsx index 5db7d9b46e2..5d8715e49f4 100644 --- a/components/src/atoms/buttons/LargeButton.stories.tsx +++ b/components/src/atoms/buttons/LargeButton.stories.tsx @@ -90,3 +90,12 @@ export const AlertAlt: Story = { iconName: 'ot-check', }, } + +export const Stroke: Story = { + args: { + buttonType: 'stroke', + buttonText: 'Button text', + disabled: false, + iconName: 'ot-check', + }, +} diff --git a/components/src/atoms/buttons/LargeButton.tsx b/components/src/atoms/buttons/LargeButton.tsx index 6ceea1002a6..9476a7d693d 100644 --- a/components/src/atoms/buttons/LargeButton.tsx +++ b/components/src/atoms/buttons/LargeButton.tsx @@ -23,12 +23,17 @@ type LargeButtonTypes = | 'alert' | 'alertStroke' | 'alertAlt' + | 'stroke' interface LargeButtonProps extends StyleProps { + /** used for form submission */ + type?: 'submit' onClick?: () => void buttonType?: LargeButtonTypes buttonText: React.ReactNode iconName?: IconName disabled?: boolean + /** aria-disabled for displaying snack bar. */ + ariaDisabled?: boolean } export function LargeButton(props: LargeButtonProps): JSX.Element { @@ -36,7 +41,9 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { buttonType = 'primary', buttonText, iconName, + ariaDisabled = false, disabled = false, + type, ...buttonProps } = props @@ -52,6 +59,8 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { disabledIconColor: string focusVisibleOutlineColor: string focusVisibleBackgroundColor: string + hoverBackgroundColor?: string + hoverColor?: string activeIconColor?: string activeColor?: string } @@ -88,6 +97,8 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { disabledIconColor: COLORS.grey50, focusVisibleOutlineColor: COLORS.blue55, focusVisibleBackgroundColor: COLORS.blue55, + hoverBackgroundColor: COLORS.blue55, + hoverColor: COLORS.white, }, alertStroke: { defaultColor: COLORS.white, @@ -115,6 +126,19 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { focusVisibleOutlineColor: COLORS.blue50, focusVisibleBackgroundColor: COLORS.red40, }, + stroke: { + defaultColor: COLORS.blue50, + disabledColor: COLORS.grey50, + defaultBackgroundColor: COLORS.white, + activeBackgroundColor: COLORS.white, + disabledBackgroundColor: COLORS.white, + iconColor: COLORS.blue50, + disabledIconColor: COLORS.grey40, + focusVisibleOutlineColor: COLORS.blue55, + focusVisibleBackgroundColor: COLORS.blue55, + hoverBackgroundColor: COLORS.white, + hoverColor: COLORS.blue55, + }, } const activeColorFor = ( style: keyof typeof LARGE_BUTTON_PROPS_BY_TYPE @@ -139,6 +163,7 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { text-align: ${TYPOGRAPHY.textAlignCenter}; border-radius: ${BORDERS.borderRadiusFull}; align-items: ${ALIGN_CENTER}; + border: ${buttonType === 'stroke' ? `2px solid ${COLORS.blue50}` : 'none'}; &:active { background-color: ${ @@ -150,6 +175,21 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { ${activeIconStyle(buttonType)}; } + &:hover { + color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].hoverColor}; + background-color: ${ + LARGE_BUTTON_PROPS_BY_TYPE[buttonType].hoverBackgroundColor + }; + + border: ${ + buttonType === 'stroke' ? `2px solid ${COLORS.blue55}` : 'none' + }; + } + + &:focus-visible { + outline: 2px solid ${COLORS.blue55}; + } + &:disabled { color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledColor}; background-color: ${ @@ -157,6 +197,13 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { }; } + &[aria-disabled='true'] { + color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledColor}; + background-color: ${ + LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor + }; + } + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { cursor: default; align-items: ${ALIGN_FLEX_START}; @@ -166,7 +213,7 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { padding: ${SPACING.spacing24}; line-height: ${TYPOGRAPHY.lineHeight20}; gap: ${SPACING.spacing60}; - border: ${BORDERS.borderRadius4} solid + outline: ${BORDERS.borderRadius4} solid ${ buttonType === 'alertStroke' && !disabled ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].defaultColor @@ -175,64 +222,66 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { ${TYPOGRAPHY.pSemiBold} - #btn-icon: { - color: ${ - disabled - ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledIconColor - : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].iconColor - }; - } + #btn-icon: { + color: ${ + disabled + ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledIconColor + : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].iconColor + }; + } - &:active { - background-color: ${ - disabled - ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor - : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].activeBackgroundColor - }; - ${!disabled && activeColorFor(buttonType)}; - border: ${BORDERS.borderRadius4} solid - ${ + &:active { + background-color: ${ disabled ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].activeBackgroundColor }; - } - &:active #btn-icon { - ${activeIconStyle(buttonType)}; - } + ${!disabled && activeColorFor(buttonType)}; + outline: ${BORDERS.borderRadius4} solid + ${ + disabled + ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor + : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].activeBackgroundColor + }; + } + &:active #btn-icon { + ${activeIconStyle(buttonType)}; + } - &:focus-visible { - background-color: ${ - disabled - ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor - : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].focusVisibleBackgroundColor - }; - ${!disabled && activeColorFor(buttonType)}; - padding: calc(${SPACING.spacing24} + ${SPACING.spacing2}); - border: ${SPACING.spacing2} solid ${COLORS.transparent}; - outline: ${ - disabled - ? 'none' - : `3px solid + &:focus-visible { + background-color: ${ + disabled + ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor + : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].focusVisibleBackgroundColor + }; + ${!disabled && activeColorFor(buttonType)}; + padding: calc(${SPACING.spacing24} + ${SPACING.spacing2}); + border: ${SPACING.spacing2} solid ${COLORS.transparent}; + outline: ${ + disabled + ? 'none' + : `3px solid ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].focusVisibleOutlineColor}` - }; - background-clip: padding-box; - box-shadow: none; - } + }; + background-clip: padding-box; + box-shadow: none; + } - &:disabled { - color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledColor}; - background-color: ${ - LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor - }; - } + &:disabled { + color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledColor}; + background-color: ${ + LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor + }; + } ` return ( disabled?: boolean + iconName?: IconName isSelected?: boolean + largeDesktopBorderRadius?: boolean radioButtonType?: 'large' | 'small' subButtonLabel?: string id?: string - iconName?: IconName maxLines?: number | null + // used for mouseEnter and mouseLeave + setNoHover?: () => void + setHovered?: () => void } // used for ODD and helix @@ -38,9 +42,14 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { onChange, radioButtonType = 'large', subButtonLabel, - id = buttonLabel, + id = typeof buttonLabel === 'string' + ? buttonLabel + : `RadioButtonId_${buttonValue}`, + largeDesktopBorderRadius = false, iconName, maxLines = null, + setHovered, + setNoHover, } = props const isLarge = radioButtonType === 'large' @@ -52,6 +61,7 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { const AVAILABLE_BUTTON_STYLE = css` background: ${COLORS.blue35}; + &:hover, &:active { background-color: ${COLORS.blue40}; } @@ -61,8 +71,9 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { background: ${COLORS.blue50}; color: ${COLORS.white}; + &:hover, &:active { - background-color: ${COLORS.blue60}; + background-color: ${COLORS.blue55}; } ` @@ -73,21 +84,28 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { ` const SettingButtonLabel = styled.label` - border-radius: ${BORDERS.borderRadius40}; - cursor: pointer; - padding: 14px ${SPACING.spacing16}; - width: 100%; + border-radius: ${ + !largeDesktopBorderRadius ? BORDERS.borderRadius40 : BORDERS.borderRadius8 + }; + cursor: pointer; + padding: ${SPACING.spacing12} ${SPACING.spacing16}; + width: 100%; - ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} - ${disabled && DISABLED_BUTTON_STYLE} + ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} + ${disabled && DISABLED_BUTTON_STYLE} + + &:focus-visible { + outline: 2px solid ${COLORS.blue55}; + } @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - cursor: default; - padding: ${isLarge ? SPACING.spacing24 : SPACING.spacing20}; - border-radius: ${BORDERS.borderRadius16}; - display: ${maxLines != null ? '-webkit-box' : undefined}; + cursor: default; + padding: ${isLarge ? SPACING.spacing24 : SPACING.spacing20}; + border-radius: ${BORDERS.borderRadius16}; + display: ${maxLines != null ? '-webkit-box' : undefined}; -webkit-line-clamp: ${maxLines ?? undefined}; -webkit-box-orient: ${maxLines != null ? 'vertical' : undefined}; + word-wrap: break-word; } } ` @@ -96,6 +114,7 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { - + ) : null} - - {buttonLabel} - + {typeof buttonLabel === 'string' ? ( + + {buttonLabel} + + ) : ( + buttonLabel + )} {subButtonLabel != null ? ( ({ box-shadow: none; border-color: ${COLORS.grey30}; color: ${COLORS.grey40}; + cursor: default; } ${styleProps} diff --git a/components/src/atoms/buttons/__tests__/LargeButton.test.tsx b/components/src/atoms/buttons/__tests__/LargeButton.test.tsx index 59503443a68..70766d0cafa 100644 --- a/components/src/atoms/buttons/__tests__/LargeButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/LargeButton.test.tsx @@ -68,6 +68,18 @@ describe('LargeButton', () => { ) }) + it('renders the stroke button', () => { + props = { + ...props, + buttonType: 'stroke', + } + render(props) + expect(screen.getByRole('button')).toHaveStyle( + `background-color: ${COLORS.white}` + ) + expect(screen.getByRole('button')).toHaveStyle(`color: ${COLORS.blue50}`) + }) + it('renders the button as disabled', () => { props = { ...props, diff --git a/components/src/atoms/buttons/__tests__/RadioButton.test.tsx b/components/src/atoms/buttons/__tests__/RadioButton.test.tsx index 75fbf673bd5..aa7ddf0771e 100644 --- a/components/src/atoms/buttons/__tests__/RadioButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/RadioButton.test.tsx @@ -29,7 +29,9 @@ describe('RadioButton', () => { render(props) const label = screen.getByRole('label') expect(label).toHaveStyle(`background-color: ${COLORS.blue35}`) - expect(label).toHaveStyle(`padding: 14px ${SPACING.spacing16}`) + expect(label).toHaveStyle( + `padding: ${SPACING.spacing12} ${SPACING.spacing16}` + ) }) it('renders the large selected button', () => { @@ -41,7 +43,9 @@ describe('RadioButton', () => { render(props) const label = screen.getByRole('label') expect(label).toHaveStyle(`background-color: ${COLORS.blue50}`) - expect(label).toHaveStyle(`padding: 14px ${SPACING.spacing16}`) + expect(label).toHaveStyle( + `padding: ${SPACING.spacing12} ${SPACING.spacing16}` + ) }) it('renders the small button with an icon', () => { @@ -53,7 +57,9 @@ describe('RadioButton', () => { render(props) const label = screen.getByRole('label') expect(label).toHaveStyle(`background-color: ${COLORS.blue35}`) - expect(label).toHaveStyle(`padding: 14px ${SPACING.spacing16}`) + expect(label).toHaveStyle( + `padding: ${SPACING.spacing12} ${SPACING.spacing16}` + ) }) it('renders the small selected button', () => { @@ -65,7 +71,9 @@ describe('RadioButton', () => { render(props) const label = screen.getByRole('label') expect(label).toHaveStyle(`background-color: ${COLORS.blue50}`) - expect(label).toHaveStyle(`padding: 14px ${SPACING.spacing16}`) + expect(label).toHaveStyle( + `padding: ${SPACING.spacing12} ${SPACING.spacing16}` + ) }) it('renders id instead of buttonLabel when id is set', () => { @@ -82,6 +90,7 @@ describe('RadioButton', () => { expect(idRadioButton).toBeInTheDocument() const buttonLabelIdRadioButton = getById( render(props).container, + // @ts-expect-error(ja, 8/23/24): buttonLabel is a string type props.buttonLabel ) expect(buttonLabelIdRadioButton).not.toBeInTheDocument() diff --git a/components/src/atoms/index.ts b/components/src/atoms/index.ts index 15598f85870..40107c74064 100644 --- a/components/src/atoms/index.ts +++ b/components/src/atoms/index.ts @@ -2,6 +2,7 @@ export * from './buttons' export * from './Checkbox' export * from './CheckboxField' export * from './Chip' +export * from './Divider' export * from './InputField' export * from './ListButton' export * from './ListItem' diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index 5dc076b1781..9158cfe360b 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -57,6 +57,7 @@ export interface LabwareOnDeck { labwareChildren?: React.ReactNode onLabwareClick?: () => void highlight?: boolean + highlightShadow?: boolean stacked?: boolean } @@ -70,6 +71,7 @@ export interface ModuleOnDeck { moduleChildren?: React.ReactNode onLabwareClick?: () => void highlightLabware?: boolean + highlightShadowLabware?: boolean stacked?: boolean } interface BaseDeckProps { @@ -90,6 +92,8 @@ interface BaseDeckProps { svgProps?: React.ComponentProps } +const LABWARE_OFFSET_DISPLAY_THRESHOLD = 2 + export function BaseDeck(props: BaseDeckProps): JSX.Element { const { robotType, @@ -237,6 +241,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { )} <> + {/* render modules, nested labware, and overlays */} {modulesOnDeck.map( ({ moduleModel, @@ -247,7 +252,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { moduleChildren, onLabwareClick, highlightLabware, - stacked = false, + highlightShadowLabware, }) => { const slotPosition = getPositionFromSlotId( moduleLocation.slotName, @@ -275,14 +280,15 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { 'left' && moduleModel === HEATERSHAKER_MODULE_V1 } highlight={highlightLabware} + highlightShadow={highlightShadowLabware} /> ) : null} {moduleChildren} - {stacked ? : null} ) : null } )} + {/* render non-module labware and overlays */} {labwareOnDeck.map( ({ labwareLocation, @@ -292,7 +298,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { missingTips, onLabwareClick, highlight, - stacked = false, + highlightShadow, }) => { if ( labwareLocation === 'offDeck' || @@ -321,9 +327,75 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { wellFill={wellFill ?? undefined} missingTips={missingTips} highlight={highlight} + highlightShadow={highlightShadow} /> {labwareChildren} - {stacked ? : null} + + ) : null + } + )} + {/* render stacked badge on module labware */} + {modulesOnDeck.map( + ({ moduleModel, moduleLocation, stacked = false }) => { + const slotPosition = getPositionFromSlotId( + moduleLocation.slotName, + deckDef + ) + const moduleDef = getModuleDef2(moduleModel) + + const { + x: nestedLabwareOffsetX, + y: nestedLabwareOffsetY, + } = moduleDef.labwareOffset + + // labwareOffset values are more accurate than our SVG renderings, so ignore any deviations under a certain threshold + const clampedLabwareOffsetX = + Math.abs(nestedLabwareOffsetX) > LABWARE_OFFSET_DISPLAY_THRESHOLD + ? nestedLabwareOffsetX + : 0 + const clampedLabwareOffsetY = + Math.abs(nestedLabwareOffsetY) > LABWARE_OFFSET_DISPLAY_THRESHOLD + ? nestedLabwareOffsetY + : 0 + // transform to be applied to children which render within the labware interfacing surface of the module + const childrenTransform = `translate(${clampedLabwareOffsetX}, ${clampedLabwareOffsetY})` + + return slotPosition != null && stacked ? ( + + + + + + ) : null + } + )} + {/* render stacked badge on non-module labware */} + {labwareOnDeck.map( + ({ labwareLocation, definition, stacked = false }) => { + if ( + labwareLocation === 'offDeck' || + !('slotName' in labwareLocation) || + // for legacy protocols that list fixed trash as a labware, do not render + definition.parameters.loadName === + 'opentrons_1_trash_3200ml_fixed' + ) { + return null + } + + const slotPosition = getPositionFromSlotId( + labwareLocation.slotName, + deckDef + ) + + return slotPosition != null && stacked ? ( + + ) : null } diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 9f1a9506a2f..25e2bd1c3a0 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -110,6 +110,10 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { const absorbanceReaderFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === ABSORBANCE_READER_V1_FIXTURE ) + const magneticBlockStagingAreaFixtures = deckConfig.filter( + ({ cutoutFixtureId }) => + cutoutFixtureId === STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE + ) return ( 0 || - wasteChuteStagingAreaFixtures.length > 0 + wasteChuteStagingAreaFixtures.length > 0 || + magneticBlockStagingAreaFixtures.length > 0 } /> {children} diff --git a/components/src/hardware-sim/Labware/LabwareAdapter/index.tsx b/components/src/hardware-sim/Labware/LabwareAdapter/index.tsx index fc05d8b5621..476fff397f6 100644 --- a/components/src/hardware-sim/Labware/LabwareAdapter/index.tsx +++ b/components/src/hardware-sim/Labware/LabwareAdapter/index.tsx @@ -25,12 +25,18 @@ export interface LabwareAdapterProps { labwareLoadName: LabwareAdapterLoadName definition?: LabwareDefinition2 highlight?: boolean + highlightShadow?: boolean } export const LabwareAdapter = ( props: LabwareAdapterProps ): JSX.Element | null => { - const { labwareLoadName, definition, highlight = false } = props + const { + labwareLoadName, + definition, + highlight = false, + highlightShadow, + } = props const highlightOutline = highlight && definition != null ? ( ) : null + const highlightShadowOutline = + highlight && definition != null ? ( + + ) : null const SVGElement = LABWARE_ADAPTER_LOADNAME_PATHS[labwareLoadName] return ( + {/** + * render an initial shadow outline first in the DOM so that the SVG highlight shadow + * does not layer over the inside of the SVG labware adapter + */} + {highlightShadowOutline} {highlightOutline} diff --git a/components/src/hardware-sim/Labware/LabwareRender.tsx b/components/src/hardware-sim/Labware/LabwareRender.tsx index 9137a2d2f15..48d60a372d4 100644 --- a/components/src/hardware-sim/Labware/LabwareRender.tsx +++ b/components/src/hardware-sim/Labware/LabwareRender.tsx @@ -51,12 +51,16 @@ export interface LabwareRenderProps { labwareStroke?: CSSProperties['stroke'] /** adds thicker blue border with blur to labware */ highlight?: boolean + /** adds a drop shadow to the highlight border */ + highlightShadow?: boolean /** Optional callback, called with WellMouseEvent args onMouseEnter */ onMouseEnterWell?: (e: WellMouseEvent) => unknown /** Optional callback, called with WellMouseEvent args onMouseLeave */ onMouseLeaveWell?: (e: WellMouseEvent) => unknown gRef?: React.RefObject onLabwareClick?: () => void + showBorder?: boolean + strokeColor?: string } export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { @@ -90,23 +94,26 @@ export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { labwareLoadName={labwareLoadName as LabwareAdapterLoadName} definition={definition} highlight={props.highlight} + highlightShadow={props.highlightShadow} /> ) } - return ( {props.wellStroke != null ? ( { ) : null} {props.disabledWells != null diff --git a/components/src/hardware-sim/Labware/labwareInternals/FilledWells.tsx b/components/src/hardware-sim/Labware/labwareInternals/FilledWells.tsx index 8db868edb9f..a8a2bfc14a2 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/FilledWells.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/FilledWells.tsx @@ -8,10 +8,11 @@ import type { CSSProperties } from 'styled-components' export interface FilledWellsProps { definition: LabwareDefinition2 fillByWell: Record + strokeColor?: string } function FilledWellsComponent(props: FilledWellsProps): JSX.Element { - const { definition, fillByWell } = props + const { definition, fillByWell, strokeColor = COLORS.black90 } = props return ( <> {map, React.ReactNode>( @@ -23,7 +24,7 @@ function FilledWellsComponent(props: FilledWellsProps): JSX.Element { wellName={wellName} well={definition.wells[wellName]} fill={color} - stroke={COLORS.black90} + stroke={strokeColor} strokeWidth="0.6" /> ) diff --git a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx index 743743bd6c0..84bfa2cfd16 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx @@ -16,6 +16,8 @@ export interface LabwareOutlineProps { isTiprack?: boolean /** adds thicker blue border with blur to labware, defaults to false */ highlight?: boolean + /** adds a drop shadow to the highlight border */ + highlightShadow?: boolean /** [legacy] override the border color */ stroke?: CSSProperties['stroke'] fill?: CSSProperties['fill'] @@ -31,6 +33,7 @@ export function LabwareOutline(props: LabwareOutlineProps): JSX.Element { height = SLOT_RENDER_HEIGHT, isTiprack = false, highlight = false, + highlightShadow = false, stroke, fill, showRadius = true, @@ -52,31 +55,41 @@ export function LabwareOutline(props: LabwareOutlineProps): JSX.Element { <> - + {/* * + * TODO(bh, 2024-08-23): layer drop shadow filters to mimic CSS box shadow - may need to evaluate performance + * https://stackoverflow.com/questions/22486039/css3-filter-drop-shadow-spread-property-alternatives + * */} + + + - {/* TODO(bh, 2024-07-22): adjust gaussian blur for stacks */} - ) : ( void /** Optional callback to be executed when mouse enters a well element */ @@ -25,6 +27,9 @@ export interface StaticLabwareProps { onMouseLeaveWell?: (e: WellMouseEvent) => unknown fill?: CSSProperties['fill'] showRadius?: boolean + wellStroke?: WellStroke + /** optional show of labware border, defaulted to true */ + showBorder?: boolean } const TipDecoration = React.memo(function TipDecoration(props: { @@ -55,24 +60,30 @@ export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element { const { definition, highlight, + highlightShadow, onLabwareClick, onMouseEnterWell, onMouseLeaveWell, fill, showRadius = true, + wellStroke = {}, + showBorder = true, } = props const { isTiprack } = definition.parameters return ( - - - + {!showBorder ? null : ( + + + + )} {flatMap( definition.ordering, @@ -89,6 +100,7 @@ export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element { ? STYLE_BY_WELL_CONTENTS.tipPresent : STYLE_BY_WELL_CONTENTS.defaultWell)} fill={fill} + stroke={wellStroke[wellName] ?? undefined} /> {isTiprack ? ( diff --git a/components/src/hardware-sim/Labware/labwareInternals/StyledWells.tsx b/components/src/hardware-sim/Labware/labwareInternals/StyledWells.tsx index 36e2011e581..c72a621aa3d 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/StyledWells.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/StyledWells.tsx @@ -26,8 +26,8 @@ export const STYLE_BY_WELL_CONTENTS: { } = { highlightedWell: { stroke: COLORS.blue50, - fill: `${COLORS.blue50}33`, // 20% opacity - strokeWidth: 1, + fill: COLORS.transparent, + strokeWidth: 0.5, }, disabledWell: { stroke: '#C6C6C6', // LEGACY --light-grey-hover @@ -37,7 +37,7 @@ export const STYLE_BY_WELL_CONTENTS: { selectedWell: { stroke: COLORS.blue50, fill: COLORS.transparent, - strokeWidth: 1, + strokeWidth: 0.5, }, tipMissing: { stroke: '#A4A4A4', // LEGACY --c-near-black diff --git a/components/src/hardware-sim/ProtocolDeck/index.tsx b/components/src/hardware-sim/ProtocolDeck/index.tsx index 366e7f51cf5..fb172dea93d 100644 --- a/components/src/hardware-sim/ProtocolDeck/index.tsx +++ b/components/src/hardware-sim/ProtocolDeck/index.tsx @@ -92,6 +92,10 @@ export function ProtocolDeck(props: ProtocolDeckProps): JSX.Element | null { ) : null, highlightLabware: handleLabwareClick != null, + highlightShadowLabware: + handleLabwareClick != null && + topLabwareDefinition != null && + topLabwareId != null, onLabwareClick: handleLabwareClick != null && topLabwareDefinition != null && @@ -140,6 +144,7 @@ export function ProtocolDeck(props: ProtocolDeckProps): JSX.Element | null { ) : null, highlight: handleLabwareClick != null, + highlightShadow: handleLabwareClick != null && isLabwareInStack, onLabwareClick: handleLabwareClick != null ? () => { diff --git a/components/src/hooks/useConditionalConfirm.ts b/components/src/hooks/useConditionalConfirm.ts index 6c010d3a925..2e9424ab75e 100644 --- a/components/src/hooks/useConditionalConfirm.ts +++ b/components/src/hooks/useConditionalConfirm.ts @@ -36,14 +36,16 @@ import { useState } from 'react' * ``` */ -export const useConditionalConfirm = ( - handleContinue: (...args: T) => any, - shouldBlock: boolean -): { +export interface UseConditionalConfirmResult { confirm: (...args: T) => void showConfirmation: boolean cancel: () => unknown -} => { +} + +export const useConditionalConfirm = ( + handleContinue: (...args: T) => any, + shouldBlock: boolean +): UseConditionalConfirmResult => { const [pendingArgs, setPendingArgs] = useState(null) const pendingConfirm = pendingArgs !== null const confirm: (...args: T) => void = (...confirmArgs) => { diff --git a/components/src/icons/icon-data.ts b/components/src/icons/icon-data.ts index cee774169ae..dfdc8b1e4e4 100644 --- a/components/src/icons/icon-data.ts +++ b/components/src/icons/icon-data.ts @@ -277,6 +277,11 @@ export const ICON_DATA_BY_NAME = { 'M20 36.6667C19.0556 36.6667 18.2569 36.3403 17.6042 35.6875C16.9514 35.0347 16.625 34.2361 16.625 33.2917H23.375C23.375 34.2361 23.0486 35.0347 22.3958 35.6875C21.7431 36.3403 20.9444 36.6667 20 36.6667ZM13.25 30.7083V28.2083H26.75V30.7083H13.25ZM13.4583 25.6667C11.625 24.4722 10.1736 22.9792 9.10417 21.1875C8.03472 19.3958 7.5 17.375 7.5 15.125C7.5 11.7361 8.73611 8.80556 11.2083 6.33333C13.6806 3.86111 16.6111 2.625 20 2.625C23.3889 2.625 26.3194 3.86111 28.7917 6.33333C31.2639 8.80556 32.5 11.7361 32.5 15.125C32.5 17.375 31.9722 19.3958 30.9167 21.1875C29.8611 22.9792 28.4028 24.4722 26.5417 25.6667H13.4583ZM14.375 23.1667H25.6667C27 22.2778 28.0556 21.125 28.8333 19.7083C29.6111 18.2917 30 16.7639 30 15.125C30 12.375 29.0208 10.0208 27.0625 8.0625C25.1042 6.10417 22.75 5.125 20 5.125C17.25 5.125 14.8958 6.10417 12.9375 8.0625C10.9792 10.0208 10 12.375 10 15.125C10 16.7639 10.3889 18.2917 11.1667 19.7083C11.9444 21.125 13.0139 22.2778 14.375 23.1667Z', viewBox: '0 0 40 40', }, + liquid: { + path: + 'M9.89583 16.1667C10.0625 16.1528 10.2049 16.0868 10.3229 15.9687C10.441 15.8507 10.5 15.7083 10.5 15.5417C10.5 15.3472 10.4375 15.191 10.3125 15.0729C10.1875 14.9549 10.0278 14.9028 9.83333 14.9167C9.26389 14.9583 8.65972 14.8021 8.02083 14.4479C7.38194 14.0937 6.97917 13.4514 6.8125 12.5208C6.78472 12.3681 6.71181 12.2431 6.59375 12.1458C6.47569 12.0486 6.34028 12 6.1875 12C5.99306 12 5.83333 12.0729 5.70833 12.2187C5.58333 12.3646 5.54167 12.5347 5.58333 12.7292C5.81944 13.9931 6.375 14.8958 7.25 15.4375C8.125 15.9792 9.00694 16.2222 9.89583 16.1667ZM9.66667 18.6667C7.76389 18.6667 6.17708 18.0139 4.90625 16.7083C3.63542 15.4028 3 13.7778 3 11.8333C3 10.4444 3.55208 8.93403 4.65625 7.30208C5.76042 5.67014 7.43056 3.90278 9.66667 2C11.9028 3.90278 13.5729 5.67014 14.6771 7.30208C15.7812 8.93403 16.3333 10.4444 16.3333 11.8333C16.3333 13.7778 15.6979 15.4028 14.4271 16.7083C13.1562 18.0139 11.5694 18.6667 9.66667 18.6667ZM9.66667 17C11.1111 17 12.3056 16.5104 13.25 15.5312C14.1944 14.5521 14.6667 13.3194 14.6667 11.8333C14.6667 10.8194 14.2465 9.67361 13.4062 8.39583C12.566 7.11806 11.3194 5.72222 9.66667 4.20833C8.01389 5.72222 6.76736 7.11806 5.92708 8.39583C5.08681 9.67361 4.66667 10.8194 4.66667 11.8333C4.66667 13.3194 5.13889 14.5521 6.08333 15.5312C7.02778 16.5104 8.22222 17 9.66667 17Z', + viewBox: '0 0 20 20', + }, lock: { path: 'M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z', @@ -648,6 +653,11 @@ export const ICON_DATA_BY_NAME = { 'M8.01487 8.84912C8.47511 8.84912 8.84821 8.47603 8.84821 8.01579C8.84821 7.55555 8.47511 7.18245 8.01487 7.18245C7.55464 7.18245 7.18154 7.55555 7.18154 8.01579C7.18154 8.47603 7.55464 8.84912 8.01487 8.84912Z M8.66654 0.928711V2.36089C11.27 2.66533 13.3354 4.73075 13.6398 7.33418H15.072V8.66751H13.6398C13.3354 11.2709 11.27 13.3363 8.66654 13.6408V15.073H7.3332V13.6408C4.72979 13.3363 2.66437 11.2709 2.35992 8.66751H0.927734V7.33418H2.35992C2.66436 4.73075 4.72978 2.66533 7.3332 2.36089V0.928711H8.66654ZM12.2944 7.33418H11.6184C11.2502 7.33418 10.9518 7.63266 10.9518 8.00085C10.9518 8.36904 11.2502 8.66751 11.6184 8.66751H12.2944C12.0071 10.5336 10.5326 12.008 8.66654 12.2953V11.6194C8.66654 11.2512 8.36806 10.9527 7.99987 10.9527C7.63168 10.9527 7.3332 11.2512 7.3332 11.6194V12.2953C5.46716 12.008 3.99268 10.5336 3.70536 8.66751H4.38132C4.74951 8.66751 5.04798 8.36904 5.04798 8.00085C5.04798 7.63266 4.74951 7.33418 4.38132 7.33418H3.70536C3.99267 5.46812 5.46715 3.99364 7.3332 3.70632V4.38229C7.3332 4.75048 7.63168 5.04896 7.99987 5.04896C8.36806 5.04896 8.66654 4.75048 8.66654 4.38229V3.70632C10.5326 3.99364 12.0071 5.46812 12.2944 7.33418Z', viewBox: '0 0 16 16', }, + search: { + path: + 'M16.3333 17.5L11.0833 12.25C10.6667 12.5833 10.1875 12.8472 9.64583 13.0417C9.10417 13.2361 8.52778 13.3333 7.91667 13.3333C6.40278 13.3333 5.12153 12.809 4.07292 11.7604C3.02431 10.7118 2.5 9.43056 2.5 7.91667C2.5 6.40278 3.02431 5.12153 4.07292 4.07292C5.12153 3.02431 6.40278 2.5 7.91667 2.5C9.43056 2.5 10.7118 3.02431 11.7604 4.07292C12.809 5.12153 13.3333 6.40278 13.3333 7.91667C13.3333 8.52778 13.2361 9.10417 13.0417 9.64583C12.8472 10.1875 12.5833 10.6667 12.25 11.0833L17.5 16.3333L16.3333 17.5ZM7.91667 11.6667C8.95833 11.6667 9.84375 11.3021 10.5729 10.5729C11.3021 9.84375 11.6667 8.95833 11.6667 7.91667C11.6667 6.875 11.3021 5.98958 10.5729 5.26042C9.84375 4.53125 8.95833 4.16667 7.91667 4.16667C6.875 4.16667 5.98958 4.53125 5.26042 5.26042C4.53125 5.98958 4.16667 6.875 4.16667 7.91667C4.16667 8.95833 4.53125 9.84375 5.26042 10.5729C5.98958 11.3021 6.875 11.6667 7.91667 11.6667Z', + viewBox: '0 0 20 20', + }, send: { path: 'M6.96216 26.6667V5.33337L32.2955 16L6.96216 26.6667ZM9.62882 22.6667L25.4288 16L9.62882 9.33337V14L17.6288 16L9.62882 18V22.6667Z', diff --git a/components/src/index.ts b/components/src/index.ts index 458e95af3b2..e52904f06fd 100644 --- a/components/src/index.ts +++ b/components/src/index.ts @@ -22,6 +22,7 @@ export * from './slotmap' export * from './structure' export * from './tooltips' export * from './organisms' + // styles export * from './styles' // new ui-overhaul style vars diff --git a/components/src/interaction-enhancers/HandleKeypress.tsx b/components/src/interaction-enhancers/HandleKeypress.tsx index 10a6b804ab3..6b713dcd178 100644 --- a/components/src/interaction-enhancers/HandleKeypress.tsx +++ b/components/src/interaction-enhancers/HandleKeypress.tsx @@ -25,7 +25,22 @@ const matchHandler = (e: KeyboardEvent) => (h: KeypressHandler) => */ export class HandleKeypress extends React.Component { handlePressIfKey = (event: KeyboardEvent): void => { - this.props.handlers.filter(matchHandler(event)).forEach(h => h.onPress()) + const pressHandlers = this.props.handlers.filter(matchHandler(event)) + + // Check if any element is currently focused + const focusedElement = document.activeElement as HTMLElement + + if (pressHandlers.length > 0) { + if ( + focusedElement && + event.key === 'Enter' && + focusedElement.matches(':focus-visible') + ) { + focusedElement.click() + } else if (!focusedElement || !focusedElement.matches(':focus-visible')) { + pressHandlers.forEach(h => h.onPress()) + } + } } preventDefaultIfKey = (event: KeyboardEvent): void => { diff --git a/components/src/molecules/DeckLabel/__tests__/DeckLabel.test.tsx b/components/src/molecules/DeckLabel/__tests__/DeckLabel.test.tsx index 88a8d3a1c7f..f0af517711a 100644 --- a/components/src/molecules/DeckLabel/__tests__/DeckLabel.test.tsx +++ b/components/src/molecules/DeckLabel/__tests__/DeckLabel.test.tsx @@ -27,12 +27,12 @@ describe('DeckLabel', () => { render(props) screen.getByText('mock DeckLabel text') const deckLabel = screen.getByTestId('DeckLabel_UnSelected') - expect(deckLabel).toHaveStyle(`padding: ${SPACING.spacing4}`) + expect(deckLabel).toHaveStyle(`padding: ${SPACING.spacing2}`) expect(deckLabel).toHaveStyle(`width: ${FLEX_MAX_CONTENT}`) expect(deckLabel).toHaveStyle(`color: ${COLORS.blue50}`) - expect(deckLabel).toHaveStyle(`border-right: 3px solid ${COLORS.blue50}`) - expect(deckLabel).toHaveStyle(`border-bottom: 3px solid ${COLORS.blue50}`) - expect(deckLabel).toHaveStyle(`border-left: 3px solid ${COLORS.blue50}`) + expect(deckLabel).toHaveStyle(`border-right: 1.5px solid ${COLORS.blue50}`) + expect(deckLabel).toHaveStyle(`border-bottom: 1.5px solid ${COLORS.blue50}`) + expect(deckLabel).toHaveStyle(`border-left: 1.5px solid ${COLORS.blue50}`) expect(deckLabel).toHaveStyle(`background-color: ${COLORS.white}`) }) @@ -44,14 +44,11 @@ describe('DeckLabel', () => { render(props) screen.getByText('mock DeckLabel text') const deckLabel = screen.getByTestId('DeckLabel_UnSelected') - expect(deckLabel).toHaveStyle(`padding: ${SPACING.spacing4}`) + expect(deckLabel).toHaveStyle(`padding: ${SPACING.spacing2}`) expect(deckLabel).toHaveStyle(`width: ${FLEX_MAX_CONTENT}`) expect(deckLabel).toHaveStyle(`color: ${COLORS.blue50}`) - expect(deckLabel).toHaveStyle(`border-right: 3px solid ${COLORS.blue50}`) - expect(deckLabel).not.toHaveStyle( - `border-bottom: 3px solid ${COLORS.blue50}` - ) - expect(deckLabel).toHaveStyle(`border-left: 3px solid ${COLORS.blue50}`) + expect(deckLabel).toHaveStyle(`border-right: 1.5px solid ${COLORS.blue50}`) + expect(deckLabel).toHaveStyle(`border-left: 1.5px solid ${COLORS.blue50}`) expect(deckLabel).toHaveStyle(`background-color: ${COLORS.white}`) }) @@ -63,10 +60,10 @@ describe('DeckLabel', () => { render(props) screen.getByText('mock DeckLabel text') const deckLabel = screen.getByTestId('DeckLabel_Selected') - expect(deckLabel).toHaveStyle(`padding: ${SPACING.spacing4}`) + expect(deckLabel).toHaveStyle(`padding: ${SPACING.spacing2}`) expect(deckLabel).toHaveStyle(`width: ${FLEX_MAX_CONTENT}`) expect(deckLabel).toHaveStyle(`color: ${COLORS.white}`) - expect(deckLabel).toHaveStyle(`border: 3px solid ${COLORS.blue50}`) + expect(deckLabel).toHaveStyle(`border: 1.5px solid ${COLORS.blue50}`) expect(deckLabel).toHaveStyle(`background-color: ${COLORS.blue50}`) }) }) diff --git a/components/src/molecules/DeckLabel/index.tsx b/components/src/molecules/DeckLabel/index.tsx index 053be470a76..9daefc562e4 100644 --- a/components/src/molecules/DeckLabel/index.tsx +++ b/components/src/molecules/DeckLabel/index.tsx @@ -1,16 +1,21 @@ import * as React from 'react' import { css } from 'styled-components' +import { getModuleType } from '@opentrons/shared-data' import { Flex } from '../../primitives' -import { FLEX_MAX_CONTENT } from '../../styles' +import { ALIGN_CENTER, FLEX_MAX_CONTENT } from '../../styles' import { COLORS } from '../../helix-design-system' import { SPACING } from '../../ui-style-constants' import { StyledText } from '../../atoms' +import { ModuleIcon } from '../../icons' import type { FlattenSimpleInterpolation } from 'styled-components' +import type { ModuleModel } from '@opentrons/shared-data' export interface DeckLabelProps { text: string isSelected: boolean + moduleModel?: ModuleModel + maxWidth?: string labelBorderRadius?: string isLast?: boolean } @@ -19,10 +24,43 @@ export function DeckLabel({ text, isSelected, labelBorderRadius, + moduleModel, + maxWidth = FLEX_MAX_CONTENT, isLast = false, }: DeckLabelProps): JSX.Element { + const DECK_LABEL_BASE_STYLE = ( + labelBorderRadius?: string + ): FlattenSimpleInterpolation => css` + width: ${FLEX_MAX_CONTENT}; + max-width: ${maxWidth}; + padding: ${SPACING.spacing2}; + border-radius: ${labelBorderRadius ?? '0'}; + ` + const DECK_LABEL_SELECTED_STYLE = ( + labelBorderRadius?: string + ): FlattenSimpleInterpolation => css` + ${DECK_LABEL_BASE_STYLE(labelBorderRadius)} + color: ${COLORS.white}; + border: 1.5px solid ${COLORS.blue50}; + background-color: ${COLORS.blue50}; + ` + + const DECK_LABEL_UNSELECTED_STYLE = ( + labelBorderRadius?: string, + isLast?: boolean + ): FlattenSimpleInterpolation => css` + ${DECK_LABEL_BASE_STYLE(labelBorderRadius)} + color: ${COLORS.blue50}; + border-right: 1.5px solid ${COLORS.blue50}; + border-bottom: 1.5px solid ${COLORS.blue50}; + border-left: 1.5px solid ${COLORS.blue50}; + background-color: ${COLORS.white}; + border-radius: ${isLast ? labelBorderRadius : '0'}; + ` + return ( - - {text} - + + {moduleModel != null ? ( + + ) : null} + + {text} + + ) } - -const DECK_LABEL_BASE_STYLE = ( - labelBorderRadius?: string -): FlattenSimpleInterpolation => css` - width: ${FLEX_MAX_CONTENT}; - padding: ${SPACING.spacing4}; - border-radius: ${labelBorderRadius ?? '0'}; -` - -const DECK_LABEL_SELECTED_STYLE = ( - labelBorderRadius?: string -): FlattenSimpleInterpolation => css` - ${DECK_LABEL_BASE_STYLE(labelBorderRadius)} - color: ${COLORS.white}; - border: 3px solid ${COLORS.blue50}; - background-color: ${COLORS.blue50}; -` - -const DECK_LABEL_UNSELECTED_STYLE = ( - labelBorderRadius?: string, - isLast?: boolean -): FlattenSimpleInterpolation => css` - ${DECK_LABEL_BASE_STYLE(labelBorderRadius)} - color: ${COLORS.blue50}; - border-right: 3px solid ${COLORS.blue50}; - border-bottom: ${isLast ? `3px solid ${COLORS.blue50}` : undefined}; - border-left: 3px solid ${COLORS.blue50}; - background-color: ${COLORS.white}; - border-radius: ${isLast ? labelBorderRadius : '0'}; -` diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index 75324f6f874..3034cb33f3c 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -7,7 +7,9 @@ import { DIRECTION_COLUMN, DIRECTION_ROW, JUSTIFY_SPACE_BETWEEN, + NO_WRAP, OVERFLOW_AUTO, + OVERFLOW_HIDDEN, POSITION_ABSOLUTE, POSITION_RELATIVE, } from '../../styles' @@ -19,6 +21,7 @@ import { useOnClickOutside } from '../../interaction-enhancers' import { LegacyStyledText } from '../../atoms/StyledText/LegacyStyledText' import { MenuItem } from '../../atoms/MenuList/MenuItem' import { Tooltip } from '../../atoms/Tooltip' +import { LiquidIcon } from '../LiquidIcon' /** this is the max height to display 10 items */ const MAX_HEIGHT = 316 @@ -29,6 +32,8 @@ const HEIGHT_ADJUSTMENT = 100 export interface DropdownOption { name: string value: string + /** optional dropdown option for adding the liquid color icon */ + liquidColor?: string } export type DropdownBorder = 'rounded' | 'neutral' @@ -74,7 +79,6 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { const [dropdownPosition, setDropdownPosition] = React.useState< 'top' | 'bottom' >('bottom') - const dropDownMenuWrapperRef = useOnClickOutside({ onClickOutside: () => { setShowDropdownMenu(false) @@ -164,7 +168,6 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { color: ${COLORS.grey40}; } ` - return ( {title !== null ? ( @@ -200,18 +203,23 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { css={DROPDOWN_STYLE} tabIndex={tabIndex} > - - {currentOption.name} - + + {currentOption.liquidColor != null ? ( + + ) : null} + + {currentOption.name} + + {showDropdownMenu ? ( ) : ( @@ -241,7 +249,12 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { setShowDropdownMenu(false) }} > - {option.name} + + {option.liquidColor != null ? ( + + ) : null} + {option.name} + ))} diff --git a/components/src/molecules/InfoScreen/InfoScreen.stories.tsx b/components/src/molecules/InfoScreen/InfoScreen.stories.tsx new file mode 100644 index 00000000000..b854dd74652 --- /dev/null +++ b/components/src/molecules/InfoScreen/InfoScreen.stories.tsx @@ -0,0 +1,49 @@ +import * as React from 'react-remove-scroll' +import { Flex } from '../../primitives' +import { SPACING } from '../../ui-style-constants' +import { COLORS } from '../../helix-design-system' +import { InfoScreen as InfoScreenComponent } from './index' + +import type { Meta, StoryObj } from '@storybook/react' + +const mockContent = [ + 'No deck hardware', + 'No labware', + 'No liquids', + 'No protocol files included', +] + +const meta: Meta = { + title: 'Library/Molecules/InfoScreen', + component: InfoScreenComponent, + argTypes: { + content: { + control: { + type: 'select', + }, + options: mockContent, + }, + backgroundColor: { + control: { + type: 'select', + }, + options: COLORS, + }, + }, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta + +type Story = StoryObj + +export const InfoScreen: Story = { + args: { + content: 'No protocol files included', + }, +} diff --git a/components/src/molecules/InfoScreen/__tests__/InfoScreen.test.tsx b/components/src/molecules/InfoScreen/__tests__/InfoScreen.test.tsx new file mode 100644 index 00000000000..0f5555e52ba --- /dev/null +++ b/components/src/molecules/InfoScreen/__tests__/InfoScreen.test.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../testing/utils' +import { BORDERS, COLORS } from '../../../helix-design-system' +import { InfoScreen } from '..' + +const render = (props: React.ComponentProps) => { + return renderWithProviders() +} + +describe('InfoScreen', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + content: 'mock info text', + } + }) + + it('should render text and icon with proper color - labware', () => { + render(props) + screen.getByLabelText('alert') + screen.getByText('mock info text') + }) + + it('should have proper styles', () => { + render(props) + expect(screen.getByTestId('InfoScreen')).toHaveStyle( + `background-color: ${COLORS.grey30}` + ) + expect(screen.getByTestId('InfoScreen')).toHaveStyle( + `border-radius: ${BORDERS.borderRadius8}` + ) + expect(screen.getByLabelText('alert')).toHaveStyle( + `color: ${COLORS.grey60}` + ) + }) +}) diff --git a/components/src/molecules/InfoScreen/index.tsx b/components/src/molecules/InfoScreen/index.tsx new file mode 100644 index 00000000000..9b976f227ee --- /dev/null +++ b/components/src/molecules/InfoScreen/index.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' + +import { BORDERS, COLORS } from '../../helix-design-system' +import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' +import { LegacyStyledText } from '../../atoms/StyledText' +import { Icon } from '../../icons' +import { Flex } from '../../primitives' +import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' + +interface InfoScreenProps { + content: string + backgroundColor?: string +} + +export function InfoScreen({ + content, + backgroundColor = COLORS.grey30, +}: InfoScreenProps): JSX.Element { + return ( + + + + {content} + + + ) +} diff --git a/components/src/molecules/LiquidIcon/LiquidIcon.stories.tsx b/components/src/molecules/LiquidIcon/LiquidIcon.stories.tsx index 33b3285586e..e72eb633dc0 100644 --- a/components/src/molecules/LiquidIcon/LiquidIcon.stories.tsx +++ b/components/src/molecules/LiquidIcon/LiquidIcon.stories.tsx @@ -42,6 +42,7 @@ export const MediumIcon: Story = { args: { size: 'medium', color: 'green', + onClick: () => {}, }, } diff --git a/components/src/molecules/LiquidIcon/index.tsx b/components/src/molecules/LiquidIcon/index.tsx index d30c770be65..ca913c48491 100644 --- a/components/src/molecules/LiquidIcon/index.tsx +++ b/components/src/molecules/LiquidIcon/index.tsx @@ -1,30 +1,41 @@ import * as React from 'react' -import { Flex } from '../../primitives' +import { css } from 'styled-components' +import { Btn, Flex } from '../../primitives' import { SPACING } from '../../ui-style-constants' import { BORDERS, COLORS } from '../../helix-design-system' -import { css } from 'styled-components' import { Icon } from '../../icons' +import { FLEX_MAX_CONTENT } from '../../styles' type LiquidIconSize = 'small' | 'medium' interface LiquidIconProps { color: string size?: LiquidIconSize + onClick?: () => void + hasError?: boolean } -const LIQUID_ICON_CONTAINER_STYLE = css` - height: max-content; - width: max-content; - background-color: ${COLORS.white}; - border-style: ${BORDERS.styleSolid}; - border-width: 1px; - border-color: ${COLORS.grey30}; - border-radius: ${BORDERS.borderRadius4}; -` - export function LiquidIcon(props: LiquidIconProps): JSX.Element { - const { color, size = 'small' } = props - return ( + const { color, size = 'small', onClick, hasError = false } = props + + const LIQUID_ICON_CONTAINER_STYLE = css` + height: max-content; + width: max-content; + background-color: ${COLORS.white}; + border-style: ${BORDERS.styleSolid}; + border-width: 1px; + border-color: ${hasError ? COLORS.red50 : COLORS.grey30}; + border-radius: ${BORDERS.borderRadius4}; + + &:hover { + border-color: ${onClick != null ? COLORS.grey35 : COLORS.grey30}; + } + &:active { + border-color: ${onClick != null ? COLORS.grey40 : COLORS.grey30}; + } + ` + + const liquid = ( ) + + return onClick != null ? ( + + {liquid} + + ) : ( + liquid + ) } diff --git a/components/src/molecules/ParametersTable/InfoScreen.tsx b/components/src/molecules/ParametersTable/InfoScreen.tsx deleted file mode 100644 index 9508c250e4b..00000000000 --- a/components/src/molecules/ParametersTable/InfoScreen.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from 'react' - -import { BORDERS, COLORS } from '../../helix-design-system' -import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' -import { LegacyStyledText } from '../../atoms/StyledText' -import { Icon } from '../../icons' -import { Flex } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' - -interface InfoScreenProps { - contentType: - | 'parameters' - | 'moduleControls' - | 'runNotStarted' - | 'labware' - | 'noFiles' - | 'noLabwareOffsetData' - t?: any - backgroundColor?: string -} - -export function InfoScreen({ - contentType, - t, - backgroundColor, -}: InfoScreenProps): JSX.Element { - let bodyText: string = '' - switch (contentType) { - case 'parameters': - bodyText = - t != null - ? t('no_parameters_specified_in_protocol') - : 'No parameters specified in this protocol' - break - case 'moduleControls': - bodyText = - t != null - ? t('connect_modules_for_controls') - : 'Connect modules to see controls' - break - case 'runNotStarted': - bodyText = t != null ? t('run_never_started') : 'Run was never started' - break - case 'labware': - bodyText = 'No labware specified in this protocol' - break - case 'noFiles': - bodyText = - t != null ? t('no_files_included') : 'No protocol files included' - break - case 'noLabwareOffsetData': - bodyText = - t != null - ? t('no_offsets_available') - : 'No Labware Offset data available' - break - default: - bodyText = contentType - } - - return ( - - - - {bodyText} - - - ) -} diff --git a/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx b/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx deleted file mode 100644 index 88a40257f80..00000000000 --- a/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react' -import { screen } from '@testing-library/react' -import { describe, it, expect, beforeEach } from 'vitest' - -import { renderWithProviders } from '../../../testing/utils' -import { BORDERS, COLORS } from '../../../helix-design-system' -import { InfoScreen } from '../InfoScreen' - -const render = (props: React.ComponentProps) => { - return renderWithProviders() -} - -describe('InfoScreen', () => { - let props: React.ComponentProps - - beforeEach(() => { - props = { - contentType: 'parameters', - } - }) - - it('should render text and icon with proper color - parameters', () => { - render(props) - screen.getByLabelText('alert') - screen.getByText('No parameters specified in this protocol') - }) - - it('should render text and icon with proper color - module controls', () => { - props = { - contentType: 'moduleControls', - } - render(props) - screen.getByLabelText('alert') - screen.getByText('Connect modules to see controls') - }) - - it('should render text and icon with proper color - run not started', () => { - props = { - contentType: 'runNotStarted', - } - render(props) - screen.getByLabelText('alert') - screen.getByText('Run was never started') - }) - - it('should render text and icon with proper color - labware', () => { - props = { - contentType: 'labware', - } - render(props) - screen.getByLabelText('alert') - screen.getByText('No labware specified in this protocol') - }) - - it('should have proper styles', () => { - render(props) - expect(screen.getByTestId('InfoScreen_parameters')).toHaveStyle( - `background-color: ${COLORS.grey30}` - ) - expect(screen.getByTestId('InfoScreen_parameters')).toHaveStyle( - `border-radius: ${BORDERS.borderRadius8}` - ) - expect(screen.getByLabelText('alert')).toHaveStyle( - `color: ${COLORS.grey60}` - ) - }) -}) diff --git a/components/src/molecules/index.ts b/components/src/molecules/index.ts index 7bfffe1b300..09fbefa3fc5 100644 --- a/components/src/molecules/index.ts +++ b/components/src/molecules/index.ts @@ -1,6 +1,7 @@ export * from './DeckInfoLabel' export * from './DropdownMenu' +export * from './InfoScreen' export * from './LiquidIcon' export * from './ParametersTable' -export * from './ParametersTable/InfoScreen' export * from './Tabs' +export * from './DeckLabel' diff --git a/components/src/organisms/DeckLabelSet/DeckLabelSet.stories.tsx b/components/src/organisms/DeckLabelSet/DeckLabelSet.stories.tsx index def1e6c3e36..feb347c09fe 100644 --- a/components/src/organisms/DeckLabelSet/DeckLabelSet.stories.tsx +++ b/components/src/organisms/DeckLabelSet/DeckLabelSet.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { BORDERS } from '../../helix-design-system' -import { Box, Flex } from '../../primitives' +import { Flex } from '../../primitives' import { SPACING } from '../../ui-style-constants' import { DeckLabelSet as DeckLabelSetComponent } from '.' @@ -38,7 +38,10 @@ type Story = StoryObj export const DeckLabel: Story = { args: { // width and height from Figma - children: , deckLabels: mockDeckLabels, + width: 31.9375, + height: 5.75, + x: 0, + y: 0, }, } diff --git a/components/src/organisms/DeckLabelSet/__tests__/DeckLabelSet.test.tsx b/components/src/organisms/DeckLabelSet/__tests__/DeckLabelSet.test.tsx index 4faae78261d..b8cdcf428ec 100644 --- a/components/src/organisms/DeckLabelSet/__tests__/DeckLabelSet.test.tsx +++ b/components/src/organisms/DeckLabelSet/__tests__/DeckLabelSet.test.tsx @@ -4,7 +4,6 @@ import { screen } from '@testing-library/react' import { renderWithProviders } from '../../../testing/utils' import { BORDERS, COLORS } from '../../../helix-design-system' -import { Box } from '../../../primitives' import { DeckLabel } from '../../../molecules/DeckLabel' import { DeckLabelSet } from '..' @@ -31,11 +30,10 @@ describe('DeckLabelSet', () => { beforeEach(() => { props = { - children: ( - - test - - ), + x: 1, + y: 1, + width: 50, + height: 50, deckLabels: mockDeckLabels, } vi.mocked(DeckLabel).mockReturnValue(
mock DeckLabels
) @@ -44,9 +42,8 @@ describe('DeckLabelSet', () => { it('should render blue border and DeckLabel', () => { render(props) expect(screen.getAllByText('mock DeckLabels').length).toBe(2) - screen.getByText('test') const deckLabelSet = screen.getByTestId('DeckLabeSet') - expect(deckLabelSet).toHaveStyle(`border: 3px solid ${COLORS.blue50}`) - expect(deckLabelSet).toHaveStyle(`border-radius: ${BORDERS.borderRadius8}`) + expect(deckLabelSet).toHaveStyle(`border: 1.5px solid ${COLORS.blue50}`) + expect(deckLabelSet).toHaveStyle(`border-radius: ${BORDERS.borderRadius4}`) }) }) diff --git a/components/src/organisms/DeckLabelSet/index.tsx b/components/src/organisms/DeckLabelSet/index.tsx index c79fb0548a6..95ff6d2f2f3 100644 --- a/components/src/organisms/DeckLabelSet/index.tsx +++ b/components/src/organisms/DeckLabelSet/index.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import styled from 'styled-components' -import { Flex } from '../../primitives' +import { Box } from '../../primitives' import { BORDERS, COLORS } from '../../helix-design-system' -import { DIRECTION_COLUMN, FLEX_MAX_CONTENT } from '../../styles' +import { RobotCoordsForeignDiv } from '../../hardware-sim' import { DeckLabel } from '../../molecules/DeckLabel' import { SPACING } from '../../ui-style-constants' @@ -10,49 +10,60 @@ import { SPACING } from '../../ui-style-constants' import type { DeckLabelProps } from '../../molecules/DeckLabel' interface DeckLabelSetProps { - children: React.ReactNode deckLabels: DeckLabelProps[] + x: number + y: number + width: number + height: number } -export function DeckLabelSet({ - children, - deckLabels, -}: DeckLabelSetProps): JSX.Element { +const DeckLabelSetComponent = ( + props: DeckLabelSetProps, + ref: React.ForwardedRef +): JSX.Element => { + const { deckLabels, x, y, width, height } = props + return ( - - {children} - + + + {deckLabels.length > 0 ? deckLabels.map((deckLabel, index) => ( )) : null} - + ) } -const StyledFlex = styled(Flex)` - width: 100%; - height: ${FLEX_MAX_CONTENT}; - border-radius: ${BORDERS.borderRadius8}; - border: 3px solid ${COLORS.blue50}; +export const DeckLabelSet = React.forwardRef( + DeckLabelSetComponent +) + +const StyledBox = styled(Box)` + border-radius: ${BORDERS.borderRadius4}; + border: 1.5px solid ${COLORS.blue50}; ` -const LabelContainer = styled(Flex)` - flex-direction: ${DIRECTION_COLUMN}; - padding-left: ${SPACING.spacing24}; +const LabelContainer = styled.div` + padding-left: ${SPACING.spacing12}; + & > *:not(:first-child):not(:last-child) { + border-bottom-right-radius: ${BORDERS.borderRadius4}; + border-top-right-radius: ${BORDERS.borderRadius4}; + } - & > *:not(:last-child) { - margin-bottom: -3px; + & > *:first-child { + border-bottom-right-radius: ${BORDERS.borderRadius4}; } & > *:last-child { - border-bottom-left-radius: ${BORDERS.borderRadius8}; - border-bottom-right-radius: ${BORDERS.borderRadius8}; + border-bottom-left-radius: ${BORDERS.borderRadius4}; + border-bottom-right-radius: ${BORDERS.borderRadius4}; } ` diff --git a/components/src/organisms/Toolbox/__tests__/Toolbox.test.tsx b/components/src/organisms/Toolbox/__tests__/Toolbox.test.tsx index 94c72c09400..d1949077f93 100644 --- a/components/src/organisms/Toolbox/__tests__/Toolbox.test.tsx +++ b/components/src/organisms/Toolbox/__tests__/Toolbox.test.tsx @@ -13,16 +13,15 @@ describe('Toolbox', () => { it('should render text and buttons', () => { props = { - title: 'header', + title:
mock header
, children:
mock children
, confirmButtonText: 'done', - titleIconName: 'swap-horizontal', onCloseClick: vi.fn(), closeButtonText: 'exit', onConfirmClick: vi.fn(), } render(props) - screen.getByText('header') + screen.getByText('mock header') screen.getByText('done') fireEvent.click(screen.getByTestId('Toolbox_confirmButton')) expect(props.onConfirmClick).toHaveBeenCalled() diff --git a/components/src/organisms/Toolbox/index.tsx b/components/src/organisms/Toolbox/index.tsx index 0bd2a494820..49258a7fa8f 100644 --- a/components/src/organisms/Toolbox/index.tsx +++ b/components/src/organisms/Toolbox/index.tsx @@ -1,27 +1,27 @@ import * as React from 'react' -import { Icon } from '../../icons' import { Box, Btn, Flex } from '../../primitives' import { ALIGN_CENTER, DIRECTION_COLUMN, JUSTIFY_SPACE_BETWEEN, + NO_WRAP, POSITION_FIXED, } from '../../styles' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING } from '../../ui-style-constants' import { PrimaryButton, StyledText } from '../../atoms' import { textDecorationUnderline } from '../../ui-style-constants/typography' -import type { IconName } from '../../icons' export interface ToolboxProps { - title: string + title: JSX.Element children: React.ReactNode confirmButtonText: string onConfirmClick: () => void onCloseClick: () => void closeButtonText: string + disableCloseButton?: boolean width?: string - titleIconName?: IconName + height?: string } export function Toolbox(props: ToolboxProps): JSX.Element { @@ -31,8 +31,9 @@ export function Toolbox(props: ToolboxProps): JSX.Element { confirmButtonText, onCloseClick, onConfirmClick, - titleIconName, closeButtonText, + height = '100%', + disableCloseButton = false, width = '19.5rem', } = props @@ -59,10 +60,10 @@ export function Toolbox(props: ToolboxProps): JSX.Element { cursor="auto" position={POSITION_FIXED} right="0" - top="0" + bottom="0" backgroundColor={COLORS.white} boxShadow="0px 3px 6px rgba(0, 0, 0, 0.23)" - height="100%" + height={height} borderRadius={BORDERS.borderRadius8} > - - {titleIconName != null ? ( - - ) : null} - {title} - + {title} {closeButtonText} diff --git a/discovery-client/package.json b/discovery-client/package.json index 3fb1a2b4731..5d73470dbe1 100644 --- a/discovery-client/package.json +++ b/discovery-client/package.json @@ -21,7 +21,7 @@ "homepage": "https://github.com/Opentrons/opentrons#readme", "dependencies": { "@types/lodash": "^4.14.191", - "@types/node-fetch": "^2.5.8", + "@types/node-fetch": "2.6.11", "@types/yargs": "17.0.32", "escape-string-regexp": "1.0.5", "is-ip": "3.1.0", diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index d4eea2f5bd3..b783908d5e6 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -173,6 +173,7 @@ def _get_liquid_probe_settings( output_option=OutputOptions.sync_only, aspirate_while_sensing=False, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files={InstrumentProbeType.PRIMARY: "/data/testing_data/pressure.csv"}, diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py index 32f41ae218d..01cb0d27375 100644 --- a/hardware-testing/hardware_testing/liquid_sense/execute.py +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -448,6 +448,7 @@ def _run_trial( output_option=OutputOptions.sync_buffer_to_csv, aspirate_while_sensing=run_args.aspirate, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files=data_files, diff --git a/hardware-testing/hardware_testing/production_qc/belt_calibration_ot3.py b/hardware-testing/hardware_testing/production_qc/belt_calibration_ot3.py index 89756200d80..0baf52934aa 100644 --- a/hardware-testing/hardware_testing/production_qc/belt_calibration_ot3.py +++ b/hardware-testing/hardware_testing/production_qc/belt_calibration_ot3.py @@ -124,11 +124,11 @@ async def _calibrate_belts( async def run_belt_calibration( - api: OT3API, mount: types.OT3Mount, test: bool + api: OT3API, mount: types.OT3Mount, calibrate: bool, test: bool ) -> Tuple[ Optional[_TestBeltCalibrationData], - AttitudeMatrix, - Dict[str, Any], + Optional[AttitudeMatrix], + Optional[Dict[str, Any]], Optional[_TestBeltCalibrationData], ]: """Run belt calibration.""" @@ -149,14 +149,16 @@ async def run_belt_calibration( if not api.is_simulator: ui.get_user_ready("ATTACH a probe to pipette") - without_data = None - with_data = None - + without_data: Optional[_TestBeltCalibrationData] = None + with_data: Optional[_TestBeltCalibrationData] = None + attitude: Optional[AttitudeMatrix] = None + details: Optional[Dict[str, Any]] = None try: # calibrate belts - ui.print_header("CALIBRATE BELTS") - await api.reset_instrument_offset(mount) - attitude, details = await _calibrate_belts(api, mount) + if calibrate: + ui.print_header("CALIBRATE BELTS") + await api.reset_instrument_offset(mount) + attitude, details = await _calibrate_belts(api, mount) # test after if test: @@ -236,7 +238,7 @@ def _create_csv_report() -> CSVReport: ) -async def run(is_simulating: bool, skip_test: bool) -> None: +async def run(is_simulating: bool, skip_calibration: bool, skip_test: bool) -> None: """Run.""" ui.print_header("BELT CALIBRATION") @@ -252,9 +254,13 @@ async def run(is_simulating: bool, skip_test: bool) -> None: helpers_ot3.set_csv_report_meta_data_ot3(api, report) # RUN TEST + before: Optional[_TestBeltCalibrationData] = None + after: Optional[_TestBeltCalibrationData] = None + attitude: Optional[AttitudeMatrix] = None + details: Optional[Dict[str, Any]] = None try: before, attitude, details, after = await run_belt_calibration( - api, types.OT3Mount.LEFT, test=not skip_test + api, types.OT3Mount.LEFT, calibrate=not skip_calibration, test=not skip_test ) except ( EarlyCapacitiveSenseTrigger, @@ -263,7 +269,6 @@ async def run(is_simulating: bool, skip_test: bool) -> None: MisalignedGantryError, ) as e: ui.print_error(str(e)) - return if api.is_simulator: nom_front_left = helpers_ot3.get_slot_calibration_square_position_ot3( @@ -283,32 +288,34 @@ async def run(is_simulating: bool, skip_test: bool) -> None: details = sim_cal_data.build_details() # STORE ATTITUDE - report("ATTITUDE", "attitude-x", attitude[0]) - report("ATTITUDE", "attitude-y", attitude[1]) - report("ATTITUDE", "attitude-z", attitude[2]) + if attitude: + report("ATTITUDE", "attitude-x", attitude[0]) + report("ATTITUDE", "attitude-y", attitude[1]) + report("ATTITUDE", "attitude-z", attitude[2]) # STORE DETAILS - report( - "BELT-CALIBRATION-POSITIONS", - "slot-front-left", - list(details["slots"]["front_left"]), - ) - report( - "BELT-CALIBRATION-POSITIONS", - "slot-front-right", - list(details["slots"]["front_right"]), - ) - report( - "BELT-CALIBRATION-POSITIONS", - "slot-rear-left", - list(details["slots"]["rear_left"]), - ) - for align_shift in AlignmentShift: + if details: + report( + "BELT-CALIBRATION-POSITIONS", + "slot-front-left", + list(details["slots"]["front_left"]), + ) + report( + "BELT-CALIBRATION-POSITIONS", + "slot-front-right", + list(details["slots"]["front_right"]), + ) report( - "BELT-CALIBRATION-SHIFTS", - align_shift.value, - [details[align_shift.value]["shift"]], + "BELT-CALIBRATION-POSITIONS", + "slot-rear-left", + list(details["slots"]["rear_left"]), ) + for align_shift in AlignmentShift: + report( + "BELT-CALIBRATION-SHIFTS", + align_shift.value, + [details[align_shift.value]["shift"]], + ) if before and after: # STORE PIPETTE-OFFSET CALIBRATIONS @@ -344,6 +351,7 @@ async def run(is_simulating: bool, skip_test: bool) -> None: if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--simulate", action="store_true") + parser.add_argument("--skip-calibration", action="store_true") parser.add_argument("--skip-test", action="store_true") args = parser.parse_args() - asyncio.run(run(args.simulate, args.skip_test)) + asyncio.run(run(args.simulate, args.skip_calibration, args.skip_test)) diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py index cb9c866eba9..28c86520d15 100644 --- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py @@ -1381,6 +1381,7 @@ async def _test_liquid_probe( output_option=OutputOptions.can_bus_only, # FIXME: remove aspirate_while_sensing=False, z_overlap_between_passes_mm=0.1, + plunger_reset_offset=2.0, samples_for_baselining=20, sample_time_sec=0.004, data_files=None, diff --git a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_belt_calibration.py b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_belt_calibration.py new file mode 100644 index 00000000000..4497e5d8c07 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_belt_calibration.py @@ -0,0 +1,22 @@ +"""Flex IQ: Belt Calibration.""" +from opentrons.protocol_api import ProtocolContext + + +metadata = {"protocolName": "Flex IQ: Belt Calibration"} +requirements = {"robotType": "Flex", "apiLevel": "2.18"} + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + plate_a1 = ctx.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt", "A1") + plate_d3 = ctx.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt", "D3") + + tips = ctx.load_labware("opentrons_flex_96_tiprack_50uL", "C2") + pipette = ctx.load_instrument("flex_1channel_1000", "right", tip_racks=[tips]) + + pipette.pick_up_tip() + pipette.move_to(plate_a1["A1"].top()) + ctx.pause() + pipette.move_to(plate_d3["H12"].top()) + ctx.pause() + pipette.return_tip() diff --git a/hardware-testing/hardware_testing/scripts/belt_calibration_ot3.py b/hardware-testing/hardware_testing/scripts/belt_calibration_ot3.py index 5f9f9d51ce3..cd4197dc941 100644 --- a/hardware-testing/hardware_testing/scripts/belt_calibration_ot3.py +++ b/hardware-testing/hardware_testing/scripts/belt_calibration_ot3.py @@ -8,6 +8,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--simulate", action="store_true") + parser.add_argument("--skip-calibration", action="store_true") parser.add_argument("--skip-test", action="store_true") args = parser.parse_args() - asyncio.run(run(args.simulate, args.skip_test)) + asyncio.run(run(args.simulate, args.skip_calibration, args.skip_test)) diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index 1d795878f9d..eb966ccebd4 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -82,11 +82,6 @@ "threshold(farads)", ] -# FIXME we should organize all of these functions to use the sensor drivers. -# FIXME we should restrict some of these functions by instrument type. - -PLUNGER_SOLO_MOVE_TIME = 0.2 - def _fix_pass_step_for_buffer( move_group: MoveGroupStep, @@ -402,6 +397,7 @@ async def liquid_probe( mount_speed: float, threshold_pascals: float, plunger_impulse_time: float, + num_baseline_reads: int, csv_output: bool = False, sync_buffer_output: bool = False, can_bus_only_output: bool = False, @@ -413,8 +409,6 @@ async def liquid_probe( log_files: Dict[SensorId, str] = {} if not data_files else data_files sensor_driver = SensorDriver() threshold_fixed_point = threshold_pascals * sensor_fixed_point_conversion - # How many samples to take to level out the sensor - num_baseline_reads = 20 sensor_binding = None if sensor_id == SensorId.BOTH and force_both_sensors: # this covers the case when we want to use both sensors in an AND configuration diff --git a/hardware/opentrons_hardware/hardware_control/tools/detector.py b/hardware/opentrons_hardware/hardware_control/tools/detector.py index 7483886c52f..c5ff9d4c1a9 100644 --- a/hardware/opentrons_hardware/hardware_control/tools/detector.py +++ b/hardware/opentrons_hardware/hardware_control/tools/detector.py @@ -82,6 +82,8 @@ async def _await_responses( node = await _handle_hepa_uv_info( response_queue, response, arbitration_id ) + seen.add(node) + break elif isinstance(response, message_definitions.ErrorMessage): log.error(f"Received error message {str(response)}") @@ -215,7 +217,6 @@ async def _resolve_with_stimulus_retries( should_respond ) expected_gripper = {NodeId.gripper}.intersection(should_respond) - expected_hepa_uv = {NodeId.hepa_uv}.intersection(should_respond) while True: pipettes, gripper, hepa_uv = await _do_tool_resolve( @@ -224,12 +225,10 @@ async def _resolve_with_stimulus_retries( output_queue.put_nowait((pipettes, gripper, hepa_uv)) seen_pipettes = set([k.application_for() for k in pipettes.keys()]) seen_gripper = set([k.application_for() for k in gripper.keys()]) - seen_hepa_uv = set([k.application_for() for k in hepa_uv.keys()]) if all( [ seen_pipettes == expected_pipettes, seen_gripper == expected_gripper, - seen_hepa_uv == expected_hepa_uv, ] ): return diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py index f4dddc8ca37..2dc7614da63 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py @@ -218,6 +218,7 @@ def move_responder( plunger_speed=8, threshold_pascals=threshold_pascals, plunger_impulse_time=0.2, + num_baseline_reads=20, csv_output=False, sync_buffer_output=False, can_bus_only_output=False, @@ -350,6 +351,7 @@ def move_responder( plunger_speed=8, threshold_pascals=14, plunger_impulse_time=0.2, + num_baseline_reads=20, csv_output=csv_output, sync_buffer_output=sync_buffer_output, can_bus_only_output=can_bus_only_output, diff --git a/labware-library/src/labware-creator/components/sections/StackingOffsets.tsx b/labware-library/src/labware-creator/components/sections/StackingOffsets.tsx index ee5c6097658..58b3b947f27 100644 --- a/labware-library/src/labware-creator/components/sections/StackingOffsets.tsx +++ b/labware-library/src/labware-creator/components/sections/StackingOffsets.tsx @@ -248,7 +248,6 @@ export function StackingOffsets(): JSX.Element | null { {isChecked ? (
route.navLinkTo !== '/createNew' - ) +export function NavigationBar(): JSX.Element | null { + const { t } = useTranslation(['shared', 'alert']) const location = useLocation() - const dispatch: ThunkDispatch = useDispatch() const navigate = useNavigate() + const dispatch: ThunkDispatch = useDispatch() const loadFile = ( fileChangeEvent: React.ChangeEvent ): void => { - if (window.confirm(t('confirm_import') as string)) { - dispatch(loadFileActions.loadProtocolFile(fileChangeEvent)) - navigate('/overview') + dispatch(loadFileActions.loadProtocolFile(fileChangeEvent)) + dispatch(toggleNewProtocolModal(false)) + } + const hasUnsavedChanges = useSelector(getHasUnsavedChanges) + + const handleCreateNew = (): void => { + if ( + !hasUnsavedChanges || + window.confirm(t('alert:confirm_create_new') as string) + ) { + dispatch(toggleNewProtocolModal(true)) + navigate('/createNew') } } - const isFilteredNavPaths = - location.pathname === '/createNew' || location.pathname === '/' - return ( + return location.pathname === '/designer' || + location.pathname === '/liquids' ? null : ( {location.pathname === '/createNew' ? null : ( - + - {t('create_new_protocol')} + {t('create_new')} - + )} - - {t('import')} - + + + {t('import')} + + + {location.pathname === '/createNew' ? null : } - {/* TODO(ja, 8/12/24: delete later. Leaving access to other - routes at all times until we make breadcrumbs and protocol overview pg */} - {isFilteredNavPaths ? null : ( - - {navRoutes.map(({ name, navLinkTo }: RouteProps) => ( - - {name} - - ))} - - )} ) } -const NavbarLink = styled(NavLink)` - color: ${COLORS.black90}; - text-decoration: none; - align-self: ${ALIGN_CENTER}; - &:hover { - color: ${COLORS.black70}; - } -` - const StyledLabel = styled.label` height: 20px; cursor: pointer; diff --git a/protocol-designer/src/ProtocolEditor.tsx b/protocol-designer/src/ProtocolEditor.tsx index e66bb285e12..519b4de5737 100644 --- a/protocol-designer/src/ProtocolEditor.tsx +++ b/protocol-designer/src/ProtocolEditor.tsx @@ -1,18 +1,11 @@ import * as React from 'react' import cx from 'classnames' import { DndProvider } from 'react-dnd' -import { BrowserRouter } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' +import { HashRouter } from 'react-router-dom' +import { useSelector } from 'react-redux' import { HTML5Backend } from 'react-dnd-html5-backend' -import { - DIRECTION_COLUMN, - DIRECTION_ROW, - Flex, - PrimaryButton, - SPACING, -} from '@opentrons/components' +import { DIRECTION_COLUMN, Flex } from '@opentrons/components' import { getEnableRedesign } from './feature-flags/selectors' -import { setFeatureFlags } from './feature-flags/actions' import { ComputingSpinner } from './components/ComputingSpinner' import { ConnectedNav } from './containers/ConnectedNav' import { Sidebar } from './containers/ConnectedSidebar' @@ -26,7 +19,7 @@ import { FileUploadMessageModal } from './components/modals/FileUploadMessageMod import { LabwareUploadMessageModal } from './components/modals/LabwareUploadMessageModal/LabwareUploadMessageModal' import { GateModal } from './components/modals/GateModal' import { CreateFileWizard } from './components/modals/CreateFileWizard' -import { AnnouncementModal } from './components/modals/AnnouncementModal' +import { AnnouncementModal } from './organisms' import { ProtocolRoutes } from './ProtocolRoutes' import styles from './components/ProtocolEditor.module.css' @@ -37,25 +30,15 @@ const showGateModal = function ProtocolEditorComponent(): JSX.Element { const enableRedesign = useSelector(getEnableRedesign) - const dispatch = useDispatch() return (
{enableRedesign ? ( - - { - dispatch(setFeatureFlags({ OT_PD_ENABLE_REDESIGN: false })) - }} - > - turn off redesign - - - + - + ) : (
diff --git a/protocol-designer/src/ProtocolRoutes.tsx b/protocol-designer/src/ProtocolRoutes.tsx index a0c59d62225..6917f4ef69f 100644 --- a/protocol-designer/src/ProtocolRoutes.tsx +++ b/protocol-designer/src/ProtocolRoutes.tsx @@ -7,11 +7,16 @@ import { Liquids } from './pages/Liquids' import { Designer } from './pages/Designer' import { CreateNewProtocolWizard } from './pages/CreateNewProtocolWizard' import { NavigationBar } from './NavigationBar' -import { Kitchen } from './organisms' +import { Settings } from './pages/Settings' +import { + Kitchen, + FileUploadMessagesModal, + LabwareUploadModal, + AnnouncementModal, +} from './organisms' import type { RouteProps } from './types' -const LANDING_ROUTE = '/' const pdRoutes: RouteProps[] = [ { Component: ProtocolOverview, @@ -37,6 +42,12 @@ const pdRoutes: RouteProps[] = [ navLinkTo: '/createNew', path: '/createNew', }, + { + Component: Settings, + name: 'Settings', + navLinkTo: '/settings', + path: '/settings', + }, ] export function ProtocolRoutes(): JSX.Element { @@ -50,14 +61,17 @@ export function ProtocolRoutes(): JSX.Element { return ( <> - + + + + {allRoutes.map(({ Component, path }: RouteProps) => { return } /> })} - } /> + } /> diff --git a/protocol-designer/src/__tests__/NavigationBar.test.tsx b/protocol-designer/src/__tests__/NavigationBar.test.tsx new file mode 100644 index 00000000000..322181be251 --- /dev/null +++ b/protocol-designer/src/__tests__/NavigationBar.test.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach, expect } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' + +import { i18n } from '../assets/localization' +import { renderWithProviders } from '../__testing-utils__' +import { NavigationBar } from '../NavigationBar' +import { getHasUnsavedChanges } from '../load-file/selectors' +import { toggleNewProtocolModal } from '../navigation/actions' +import { SettingsIcon } from '../molecules' + +vi.mock('../molecules') +vi.mock('../navigation/actions') +vi.mock('../file-data/selectors') +vi.mock('../load-file/selectors') +const render = () => { + return renderWithProviders( + + + , + { i18nInstance: i18n } + ) +} + +describe('NavigationBar', () => { + beforeEach(() => { + vi.mocked(getHasUnsavedChanges).mockReturnValue(false) + vi.mocked(SettingsIcon).mockReturnValue(
mock SettingsIcon
) + }) + it('should render text and link button', () => { + render() + screen.getByText('Opentrons') + screen.getByText('Protocol Designer') + screen.getByText('Version # fake_PD_version') + screen.getByText('Create new') + screen.getByText('Import') + screen.getByText('mock SettingsIcon') + }) + + it('when clicking Create new, should call the toggle action', () => { + render() + fireEvent.click(screen.getByText('Create new')) + expect(vi.mocked(toggleNewProtocolModal)).toHaveBeenCalled() + }) + + it.todo('when clicking Import, mock function should be called', () => {}) +}) diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index c077b617c07..0a832517db9 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -9,30 +9,34 @@ "description": "Description", "edit": "Edit", "fixtures_added": "Fixtures added", + "fixtures_replace": "Fixtures replace standard deck slots and let you add functionality to your Flex.", + "incompatible_tip_body": "Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.", + "incompatible_tips": "Incompatible tips", + "labware_name": "Labware name", "modules_added": "Modules added", "name": "Name", "need_gripper": "Do you want to move labware automatically with the gripper?", - "fixtures_replace": "Fixtures replace standard deck slots and let you add functionality to your Flex.", "pip_gen": "Pipette generation", "pip_tips": "Pipette tips", "pip_type": "Pipette type", "pip_vol": "Pipette volume", "pip": "{{mount}} pipette", "quantity": "Quantity", - "questions": "We’re going to ask a few questions to help you get started building your protocol.", "remove": "Remove", + "rename_error": "Labware names must be 115 characters or fewer.", + "rename_labware": "Rename labware", "robot_pips": "Robot pipettes", "robot_type": "What kind of robot do you have?", "show_all_tips": "Show all tips", "show_default_tips": "Show default tips", - "stagingArea_cutoutA3": "Staging area A3", - "stagingArea_cutoutB3": "Staging area B3", - "stagingArea_cutoutC3": "Staging area C3", - "stagingArea_cutoutD3": "Staging area D3", + "show_tips": "Show incompatible tips", + "slots_limit_reached": "Slots limit reached", + "stagingArea": "Staging area", "swap": "Swap pipettes", "tell_us": "Tell us about your protocol", - "up_to_3_tipracks": "Up to 3 tip rack types are allowed per pipette", + "trash_required": "A trash entity is required", "trashBin": "Trash Bin", + "up_to_3_tipracks": "Up to 3 tip rack types are allowed per pipette", "vol_label": "{{volume}} uL", "wasteChute": "Waste Chute", "which_fixtures": "Which fixtures will you be using?", diff --git a/protocol-designer/src/assets/localization/en/liquids.json b/protocol-designer/src/assets/localization/en/liquids.json index 761f8b0fb2a..628ae82b215 100644 --- a/protocol-designer/src/assets/localization/en/liquids.json +++ b/protocol-designer/src/assets/localization/en/liquids.json @@ -1,3 +1,18 @@ { - "liquids": "Liquids" + "add_liquid": "Add liquid", + "add": "Add", + "clear_wells": "Clear wells", + "click_and_drag": "Click and drag to select wells", + "define_liquid": "Define a liquid", + "delete_liquid": "Delete liquid", + "description": "Description", + "display_color": "Color", + "liquid_volume": "Liquid volume by well", + "liquid": "Liquid", + "liquids_added": "Liquids added", + "liquids": "Liquids", + "microliters": "µL", + "name": "Name", + "save": "Save", + "well": "Well" } diff --git a/protocol-designer/src/assets/localization/en/modal.json b/protocol-designer/src/assets/localization/en/modal.json index 08bade5fbeb..1115b90a18b 100644 --- a/protocol-designer/src/assets/localization/en/modal.json +++ b/protocol-designer/src/assets/localization/en/modal.json @@ -50,6 +50,13 @@ "body4": "Assign up to three types of tip racks to a single pipette.", "body5": "Add multiple Temperature Modules to the deck (Flex only).", "body6": "All protocols require {{app}} version 7.3.0 or later to run." + }, + "redesign": { + "body1": "Welcome to Protocol Designer 9.0.0!", + "body2": "We’re excited to release the new Opentrons Protocol Designer, now with a fresh redesign! Enjoy the same functionality with some powerful new features:", + "body3": "Easily group multiple steps together, name the group, and keep your protocols organized with step grouping.", + "body4": "Add multiple Heater-Shaker Modules and Magnetic Blocks to the deck (Flex only).", + "body5": "All protocols now require Opentrons App version 8.0.0+ to run." } }, "labware_selection": { diff --git a/protocol-designer/src/assets/localization/en/protocol_overview.json b/protocol-designer/src/assets/localization/en/protocol_overview.json index b1a3e3cc9c1..ef978507ab5 100644 --- a/protocol-designer/src/assets/localization/en/protocol_overview.json +++ b/protocol-designer/src/assets/localization/en/protocol_overview.json @@ -1,18 +1,36 @@ { + "add_gripper": "Add a gripper", + "app_version": "{{version}} or higher", "author": "Organization/Author", "created": "Date created", + "deck_hardware": "Deck hardware", "description": "Description", + "edit_protocol": "Edit protocol", "edit": "Edit", + "export_protocol": "Export protocol", "extension": "Extension mount", "gripper": "Opentrons Flex Gripper", "instruments": "Instruments", + "labware": "Labware", "left_pip": "Left pipette", + "liquid_defs": "Liquid Definitions", "liquids": "Liquids", + "materials_list": "Materials list", "modified": "Last exported", - "protocol_metadata": "Protocol metadata", + "na": "N/A", + "name": "Name", + "no_deck_hardware": "No deck hardware", + "no_labware": "No labware", + "no_liquids_defined": "No liquids defined", + "no_liquids": "No liquids", + "no_steps": "No steps defined", + "protocol_metadata": "Protocol Metadata", + "required_app_version": "Required app version", "right_pip": "Right pipette", "robotType": "Robot type", - "starting_deck": "Protocol starting deck", + "starting_deck": "Protocol Starting Deck", "step": "Protocol steps", - "untitled_protocol": "Untitled protocol" + "total_well_volume": "Total Well Volume", + "untitled_protocol": "Untitled protocol", + "your_gripper": "Your gripper" } diff --git a/protocol-designer/src/assets/localization/en/shared.json b/protocol-designer/src/assets/localization/en/shared.json index 37c5cef7e67..afe8f157fa5 100644 --- a/protocol-designer/src/assets/localization/en/shared.json +++ b/protocol-designer/src/assets/localization/en/shared.json @@ -1,44 +1,124 @@ { "add": "add", "amount": "Amount:", + "app_settings": "App settings", + "ask_for_labware_overwrite": "Duplicate labware name", "cancel": "Cancel", + "close": "Close", "confirm_import": "Are you sure you want to upload this protocol?", "confirm_reorder": "Are you sure you want to reorder these steps, it may cause errors?", "confirm": "Confirm", "create_a_protocol": "Create a protocol", - "create_new_protocol": "Create new protocol", + "create_new": "Create new", + "developer_ff": "Developer feature flags", "done": "Done", + "edit_existing": "Edit existing protocol", + "edit_instruments": "Edit instruments", + "edit_protocol_metadata": "Edit protocol metadata", "edit": "edit", "eight_channel": "8-Channel", + "exact_labware_match": "Duplicate labware definition", "exit": "exit", + "fixtures": "Fixtures", "go_back": "Go back", - "import_existing": "Import existing protocol", + "gripper": "Gripper", + "heatershakermoduletype": "Heater-shaker Module", + "hints": "Hints", "import": "Import", + "incorrect_file_header": "Invalid file type", + "incorrect_file_type_body": "Protocol Designer only accepts JSON protocol files created with Protocol Designer. Upload a valid file to continue.", + "invalid_json_file_body": "This JSON file is either missing required information or contains sections that Protocol Designer cannot read. At this time we do not support JSON files created outside of Protocol Designer.", + "invalid_json_file_error": "Error message:", + "invalid_json_file": "Invalid JSON file", + "labware_detail": "Labware detail", + "labware_name_conflict": "Duplicate labware name", "labware": "Labware", "left_right": "Left+Right", "left": "Left", "liquid": "Liquid", + "magneticmoduletype": "Magnetic Module", + "migration_header": "Your protocol was made in an older version of Protocol Designer", + "migrations": { + "noBehaviorChange": { + "body1": "Your protocol will be automatically updated to the latest version.", + "body2": "We have added new features since the last time this protocol was updated, but have not made any changes to existing protocol behavior. Because of this we do not expect any changes in how the robot will execute this protocol. To be safe we will still recommend keeping a separate copy of the file you just imported.", + "body3": "As always, please contact us with any questions or feedback." + }, + "toV8_1Migration": { + "body1": "Your protocol will be automatically updated to the latest version.", + "body2": "The default dispense height is now 1 mm from the bottom of the well, instead of 0.5 mm. All dispense commands without a custom height will change to use the new default height.", + "body3": "Additionally, blowout actions now precede touch tip actions. The new order affects all dispenses that include both actions." + }, + "toV8Migration": { + "body1": "Your protocol will be automatically updated to the latest version.", + "body2": "Protocol Designer no longer supports aspirate or mix actions in a trash bin. If your protocol contains these actions, an error icon will appear next to them in the Protocol Timeline. To resolve the error, choose another location for aspirating or mixing.", + "body3": "Additionally, we have addressed a bug where blow out speeds were slower than expected. Your protocol will automatically update the flow rates unless they were specifically initialized.", + "body4": "As always, please contact us with any questions or feedback." + }, + "generic": { + "body1": "Your protocol will be automatically updated to the latest version. Please note that the updated file will be incompatible with older versions of the Protocol Designer. We recommend making a separate copy of the file that you just imported before continuing.", + "body2": "Updating the file may make changes to liquid handling actions. Please review your file in the Protocol Designer", + "body3": "As always, please contact us with any questions or feedback." + }, + "toV3Migration": { + "title": "Update protocol to use new labware definitions", + "body1": "To import your file successfully, you must update your protocol to use the new labware definitions. Your protocol was made using an older version of Protocol Designer. Since then, Protocol Designer has been improved to include new labware definitions which are more accurate and reliable.", + "body2": "What this means for you protocol", + "body3": "Updating your protocol to use the new labware definitions will consequently require you to re-calibrate all labware in your protocol prior to running it on your robot. We recommend you try a dry run or one with water to ensure everything is working as expected.", + "body4": "What happens if you don't update", + "body5": "If you choose not to update, you will still be able to run your protocol as usual with older labware, however you will not be able to make further updates to this protocol using the Protocol Designer.", + "body6": "Please note that in order to run the updated protocol on your robot successfully, the OT-2 App and robot are required to be updated to version 3.10.0 or higher." + } + }, + "message_exact_labware_match": "This labware is identical to one you have already uploaded.", + "message_invalid_json_file": "Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator", + "message_not_json": "Protocol Designer only accepts JSON files.", + "message_only_tiprack": "This labware definition is not a tip rack.", + "message_uses_standard_namespace": "This labware definition uses the Opentrons standard labware namespace. Change the namespace if it is custom, or use the standard labware in your protocol.", + "mismatched": "The new labware has a different arrangement of wells than the labware it is replacing. Clicking Overwrite will deselect all wells in any existing steps that use this labware. You will have to edit each of those steps and select new wells.", "module": "Module", "next": "next", "ninety_six_channel": "96-Channel", - "no-code-solution": "A no-code solution to create protocols that x, y and z meaning for your lab and workflow.", + "no_hints_to_restore": "No hints to restore", + "no-code-required": "The easiest way to automate liquid handling on your Opentrons robot. No code required.", "no": "No", "none": "None", + "not_json": "Incompatible file type", "one_channel": "1-Channel", + "only_tiprack": "Incompatible file type", "opentrons_flex": "Opentrons Flex", "opentrons": "Opentrons", "ot2": "Opentrons OT-2", + "overwrite_labware": "Overwrite labware", + "overwrite": "Click Overwrite to replace the existing labware with the new labware.", + "pd_version": "Protocol designer version", + "privacy": "Privacy", "protocol_designer": "Protocol Designer", + "re_export": "To use this definition, use Labware Creator to give it a unique load name and display name.", "remove": "remove", + "reset_hints_and_tips": "Reset all hints and tips notifications", + "reset_hints": "Reset hints", "right": "Right", - "slot_stack_information": "Slot Stack Information", + "save": "Save", + "settings": "Settings", + "shared_display_name": "Shared display name: ", + "shared_load_name": "Shared load name: ", + "shared_sessions": "Share sessions with Opentrons", + "shares_name": "This labware has the same load name or display name as {{customOrStandard}}, which is already in this protocol.", + "slot_detail": "Slot Detail", "stagingArea": "Staging area", "step_count": "Step {{current}}", "step": "Step {{current}} / {{max}}", - "trashBin": "Trash bin", + "temperaturemoduletype": "Temperature Module", + "thermocyclermoduletype": "Thermocycler Module", + "trashBin": "Trash Bin", + "user_settings": "User settings", + "uses_standard_namespace": "Opentrons verified labware", "version": "Version # {{version}}", + "warning": "WARNING:", "wasteChute": "Waste chute", - "wasteChuteAndStagingArea": "Waste chute and staging area", + "wasteChuteAndStagingArea": "Waste chute and staging area slot", + "we_are_improving": "We’re working to improve Protocol Designer. Part of the process involves watching real user sessions to understand which parts of the interface are working and which could use improvement. We never share sessions outside of Opentrons.", "welcome": "Welcome to Protocol Designer", "yes": "Yes" } diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index 5c6fb953539..6886098ddbf 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -2,21 +2,49 @@ "adapter_compatible_lab": "Adapter compatible labware", "adapter": "Adapter", "add_fixture": "Add a fixture", + "add_hw_lw": "Add hardware/labware", "add_labware": "Add labware", + "add_liquid": "Add liquid", "add_module": "Add a module", + "add_rest": "Add labware and liquids to complete deck setup", "aluminumBlock": "Aluminum block", + "clear_labware": "Clear labware", + "clear_slot": "Clear slot", "clear": "Clear", - "custom_labware": "Add custom labware", "custom": "Custom labware definitions", - "customize_slot": "Customize slot {{slotName}}", + "customize_slot": "Customize slot", "deck_hardware": "Deck hardware", + "define_liquid": "Define a liquid", "done": "Done", + "duplicate": "Duplicate labware", + "edit_hw_lw": "Edit hardware/labware", + "edit_labware": "Edit labware", + "edit_protocol": "Edit protocol", + "edit_slot": "Edit slot", "edit": "Edit", + "heater_shaker_adjacent_to": "A module is adjacent to this slot. The Heater-Shaker cannot be placed next to a module", + "heater_shaker_adjacent": "A Heater-Shaker is adjacent to this slot. Modules cannot be placed next to a Heater-Shaker", + "heater_shaker_trash": "The heater-shaker cannot be next to the trash bin", "labware": "Labware", + "liquids": "Liquids", + "no_offdeck_labware": "No off-deck labware added", + "off_deck_labware": "Off-deck Labware", + "off_deck_title": "Off deck", + "offDeck": "Off-deck", + "onDeck": "On deck", + "one_item": "No more than 1 {{hardware}} allowed on the deck at one time", + "only_display_rec": "Only display recommended labware", "protocol_starting_deck": "Protocol starting deck", + "rename_lab": "Rename labware", "reservoir": "Reservoir", "starting_deck_state": "Starting deck state", + "tc_slots_occupied_flex": "The Thermocycler needs slots A1 and B1. Slot A1 is occupied", + "tc_slots_occupied_ot2": "The Thermocycler needs slots 7, 8, 10, and 11. One or more of those slots is occupied", "tipRack": "Tip rack", + "trash_required": "A trash bin or waste chute is required", "tubeRack": "Tube rack", + "untitled_protocol": "Untitled protocol", + "upload_custom_labware": "Upload custom labware", + "we_added_hardware": "We've added your deck hardware!", "wellPlate": "Well plate" } diff --git a/protocol-designer/src/assets/localization/en/well_selection.json b/protocol-designer/src/assets/localization/en/well_selection.json index 6c730bc6a5d..b3d999dad23 100644 --- a/protocol-designer/src/assets/localization/en/well_selection.json +++ b/protocol-designer/src/assets/localization/en/well_selection.json @@ -1,4 +1,3 @@ { - "select_instructions": "Click + Drag to select multiple.", - "deselect_instructions": "Shift + Click to de-select." + "select_instructions": "Click + Drag to select multiple." } diff --git a/protocol-designer/src/atoms/constants.ts b/protocol-designer/src/atoms/constants.ts new file mode 100644 index 00000000000..e5c73333cd8 --- /dev/null +++ b/protocol-designer/src/atoms/constants.ts @@ -0,0 +1,9 @@ +import { css } from 'styled-components' +import { COLORS } from '@opentrons/components' + +export const BUTTON_LINK_STYLE = css` + color: ${COLORS.grey60}; + &:hover { + color: ${COLORS.grey40}; + } +` diff --git a/protocol-designer/src/atoms/index.ts b/protocol-designer/src/atoms/index.ts index f8f44e7744b..f87cf0102a1 100644 --- a/protocol-designer/src/atoms/index.ts +++ b/protocol-designer/src/atoms/index.ts @@ -1 +1 @@ -console.log('atoms for new components') +export * from './constants' diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 8c47450f800..52e312b37e0 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -278,9 +278,9 @@ export function v8WarningContent(t: any): JSX.Element { return (

- {t(`hint.export_v8_1_protocol_7_3.body1`)}{' '} - {t(`hint.export_v8_1_protocol_7_3.body2`)} - {t(`hint.export_v8_1_protocol_7_3.body3`)} + {t(`alert:hint.export_v8_1_protocol_7_3.body1`)}{' '} + {t(`alert:hint.export_v8_1_protocol_7_3.body2`)} + {t(`alert:hint.export_v8_1_protocol_7_3.body3`)}

) diff --git a/protocol-designer/src/components/LiquidPlacementModal.tsx b/protocol-designer/src/components/LiquidPlacementModal.tsx index d55f2b88851..64025e370db 100644 --- a/protocol-designer/src/components/LiquidPlacementModal.tsx +++ b/protocol-designer/src/components/LiquidPlacementModal.tsx @@ -55,6 +55,7 @@ export function LiquidPlacementModal(): JSX.Element | null { {labwareDef && (

{t('select_instructions')}

-

{t('deselect_instructions')}

) diff --git a/protocol-designer/src/components/labware/SelectableLabware.tsx b/protocol-designer/src/components/labware/SelectableLabware.tsx index 257fe013270..c0e24ee6218 100644 --- a/protocol-designer/src/components/labware/SelectableLabware.tsx +++ b/protocol-designer/src/components/labware/SelectableLabware.tsx @@ -2,6 +2,8 @@ import * as React from 'react' import reduce from 'lodash/reduce' import { COLUMN } from '@opentrons/shared-data' +import { COLORS } from '@opentrons/components' + import { arrayToWellGroup, getCollidingWells, @@ -11,7 +13,12 @@ import { SingleLabware } from './SingleLabware' import { SelectionRect } from '../SelectionRect' import { WellTooltip } from './WellTooltip' -import type { WellMouseEvent, WellGroup } from '@opentrons/components' +import type { + WellMouseEvent, + WellGroup, + WellFill, + WellStroke, +} from '@opentrons/components' import type { ContentsByWell } from '../../labware-ingred/types' import type { WellIngredientNames } from '../../steplist/types' import type { GenericRect } from '../../collision-types' @@ -30,6 +37,7 @@ export interface Props { nozzleType: NozzleType | null ingredNames: WellIngredientNames wellContents: ContentsByWell + showBorder: boolean } type ChannelType = 8 | 96 @@ -52,6 +60,7 @@ export const SelectableLabware = (props: Props): JSX.Element => { nozzleType, ingredNames, wellContents, + showBorder, } = props const labwareDef = labwareProps.definition @@ -121,7 +130,11 @@ export const SelectableLabware = (props: Props): JSX.Element => { rect ) => { const wells = _wellsFromSelected(_getWellsFromRect(rect)) - if (e.shiftKey) { + const areWellsAlreadySelected = Object.keys(wells).every( + well => well in selectedPrimaryWells + ) + + if (areWellsAlreadySelected) { deselectWells(wells) } else { selectWells(wells) @@ -162,6 +175,27 @@ export const SelectableLabware = (props: Props): JSX.Element => { ) : selectedPrimaryWells + const wellFillWithLiq: WellFill = {} + const wellStroke: WellStroke = {} + Object.keys(labwareDef.wells).forEach(wellName => { + wellFillWithLiq[wellName] = COLORS.blue35 + wellStroke[wellName] = COLORS.transparent + }) + Object.keys(allSelectedWells).forEach(wellName => { + wellFillWithLiq[wellName] = COLORS.blue50 + wellStroke[wellName] = COLORS.transparent + }) + Object.keys(selectedPrimaryWells).forEach(wellName => { + wellFillWithLiq[wellName] = COLORS.blue50 + wellStroke[wellName] = COLORS.transparent + }) + const wellFill = labwareProps.wellFill != null ? labwareProps.wellFill : null + if (wellFill != null) { + Object.keys(wellFill).forEach(wellName => { + wellFillWithLiq[wellName] = wellFill[wellName] + }) + } + return ( { }) => ( { handleMouseLeaveWell(mouseEventArgs) diff --git a/protocol-designer/src/components/modals/FileUploadMessageModal/FileUploadMessageModal.tsx b/protocol-designer/src/components/modals/FileUploadMessageModal/FileUploadMessageModal.tsx index 3eccb917f84..de13295063f 100644 --- a/protocol-designer/src/components/modals/FileUploadMessageModal/FileUploadMessageModal.tsx +++ b/protocol-designer/src/components/modals/FileUploadMessageModal/FileUploadMessageModal.tsx @@ -14,13 +14,12 @@ export function FileUploadMessageModal(): JSX.Element | null { const message = useSelector(loadFileSelectors.getFileUploadMessages) const dispatch = useDispatch() const { t } = useTranslation(['modal', 'button']) - - const dismissModal = (): void => { - dispatch(loadFileActions.dismissFileUploadMessage()) - } const modalContents = useModalContents({ uploadResponse: message, }) + const dismissModal = (): void => { + dispatch(loadFileActions.dismissFileUploadMessage()) + } if (modalContents == null) return null diff --git a/protocol-designer/src/components/modals/utils.ts b/protocol-designer/src/components/modals/utils.ts index 1c9d6a2a10c..4b514281bf3 100644 --- a/protocol-designer/src/components/modals/utils.ts +++ b/protocol-designer/src/components/modals/utils.ts @@ -37,15 +37,9 @@ export function getTiprackOptions(props: TiprackOptionsProps): TiprackOption[] { .filter(def => def.metadata.displayCategory === 'tipRack') .filter(def => { if (allowAllTipracks && !isFlexPipette) { - return ( - !def.metadata.displayName.includes('Flex') || - def.namespace === 'custom_beta' - ) + return !def.metadata.displayName.includes('Flex') } else if (allowAllTipracks && isFlexPipette) { - return ( - def.metadata.displayName.includes('Flex') || - def.namespace === 'custom_beta' - ) + return def.metadata.displayName.includes('Flex') } else { return ( selectedPipetteDefaultTipracks.includes(getLabwareDefURI(def)) || diff --git a/protocol-designer/src/labware-ingred/actions/actions.ts b/protocol-designer/src/labware-ingred/actions/actions.ts index 5741b06fc24..b9a36aa416c 100644 --- a/protocol-designer/src/labware-ingred/actions/actions.ts +++ b/protocol-designer/src/labware-ingred/actions/actions.ts @@ -1,7 +1,8 @@ import { createAction } from 'redux-actions' import { selectors } from '../selectors' import type { DeckSlot, ThunkAction } from '../../types' -import type { IngredInputs } from '../types' +import type { Fixture, IngredInputs } from '../types' +import type { CutoutId, ModuleModel } from '@opentrons/shared-data' // ===== Labware selector actions ===== export interface OpenAddLabwareModalAction { type: 'OPEN_ADD_LABWARE_MODAL' @@ -106,6 +107,7 @@ export interface DuplicateLabwareAction { slot: DeckSlot } } + export interface RemoveWellsContentsAction { type: 'REMOVE_WELLS_CONTENTS' payload: { @@ -214,3 +216,82 @@ export const editLiquidGroup: ( }, }) } + +// NOTE: the following actions are for selecting labware/hardware for the zoomed in slot +export interface SelectLabwareAction { + type: 'SELECT_LABWARE' + payload: { + labwareDefUri: string | null + } +} +export const selectLabware: ( + payload: SelectLabwareAction['payload'] +) => SelectLabwareAction = payload => ({ + type: 'SELECT_LABWARE', + payload, +}) +export interface SelectNestedLabwareAction { + type: 'SELECT_NESTED_LABWARE' + payload: { + nestedLabwareDefUri: string | null + } +} +export const selectNestedLabware: ( + payload: SelectNestedLabwareAction['payload'] +) => SelectNestedLabwareAction = payload => ({ + type: 'SELECT_NESTED_LABWARE', + payload, +}) + +export interface SelectModuleAction { + type: 'SELECT_MODULE' + payload: { + moduleModel: ModuleModel | null + } +} +export const selectModule: ( + payload: SelectModuleAction['payload'] +) => SelectModuleAction = payload => ({ + type: 'SELECT_MODULE', + payload, +}) + +export interface SelectFixtureAction { + type: 'SELECT_FIXTURE' + payload: { + fixture: Fixture | null + } +} +export const selectFixture: ( + payload: SelectFixtureAction['payload'] +) => SelectFixtureAction = payload => ({ + type: 'SELECT_FIXTURE', + payload, +}) + +export interface ZoomedIntoSlotAction { + type: 'ZOOMED_INTO_SLOT' + payload: { + slot: DeckSlot | null + cutout: CutoutId | null + } +} +export const selectZoomedIntoSlot: ( + payload: ZoomedIntoSlotAction['payload'] +) => ZoomedIntoSlotAction = payload => ({ + type: 'ZOOMED_INTO_SLOT', + payload, +}) + +export interface GenerateNewProtocolAction { + type: 'GENERATE_NEW_PROTOCOL' + payload: { + isNewProtocol: boolean + } +} +export const generateNewProtocol: ( + payload: GenerateNewProtocolAction['payload'] +) => GenerateNewProtocolAction = payload => ({ + type: 'GENERATE_NEW_PROTOCOL', + payload, +}) diff --git a/protocol-designer/src/labware-ingred/actions/thunks.ts b/protocol-designer/src/labware-ingred/actions/thunks.ts index 33d054f930e..dedfae883d8 100644 --- a/protocol-designer/src/labware-ingred/actions/thunks.ts +++ b/protocol-designer/src/labware-ingred/actions/thunks.ts @@ -5,12 +5,25 @@ import { selectors as stepFormSelectors } from '../../step-forms' import { selectors as uiLabwareSelectors } from '../../ui/labware' import { getNextAvailableDeckSlot, getNextNickname } from '../utils' import { getRobotType } from '../../file-data/selectors' +import { + selectNestedLabware, + selectLabware, + selectModule, + selectFixture, +} from './actions' +import type { LabwareOnDeck, ModuleOnDeck } from '../../step-forms' import type { CreateContainerArgs, CreateContainerAction, DuplicateLabwareAction, + SelectNestedLabwareAction, + SelectLabwareAction, + SelectModuleAction, + SelectFixtureAction, } from './actions' import type { ThunkAction } from '../../types' +import type { Fixture } from '../types' + export interface RenameLabwareAction { type: 'RENAME_LABWARE' payload: { @@ -167,3 +180,39 @@ export const duplicateLabware: ( }) } } + +interface EditSlotInfo { + createdModuleForSlot?: ModuleOnDeck | null + createdLabwareForSlot?: LabwareOnDeck | null + createdNestedLabwareForSlot?: LabwareOnDeck | null + preSelectedFixture?: Fixture | null +} + +export const editSlotInfo: ( + args: EditSlotInfo +) => ThunkAction< + | SelectNestedLabwareAction + | SelectLabwareAction + | SelectModuleAction + | SelectFixtureAction +> = args => dispatch => { + const { + createdModuleForSlot, + createdLabwareForSlot, + createdNestedLabwareForSlot, + preSelectedFixture, + } = args + + dispatch( + selectNestedLabware({ + nestedLabwareDefUri: createdNestedLabwareForSlot?.labwareDefURI ?? null, + }) + ) + dispatch( + selectLabware({ + labwareDefUri: createdLabwareForSlot?.labwareDefURI ?? null, + }) + ) + dispatch(selectModule({ moduleModel: createdModuleForSlot?.model ?? null })) + dispatch(selectFixture({ fixture: preSelectedFixture ?? null })) +} diff --git a/protocol-designer/src/labware-ingred/reducers/index.ts b/protocol-designer/src/labware-ingred/reducers/index.ts index b968c93b4ac..3ac4d42e0ad 100644 --- a/protocol-designer/src/labware-ingred/reducers/index.ts +++ b/protocol-designer/src/labware-ingred/reducers/index.ts @@ -10,8 +10,14 @@ import type { LocationLiquidState, LabwareLiquidState, } from '@opentrons/step-generation' +import type { LoadLabwareCreateCommand } from '@opentrons/shared-data' import type { Action, DeckSlot } from '../../types' -import type { LiquidGroupsById, DisplayLabware } from '../types' +import type { + LiquidGroupsById, + DisplayLabware, + ZoomedIntoSlotInfoState, + GenerateNewProtocolState, +} from '../types' import type { LoadFileAction } from '../../load-file' import type { RemoveWellsContentsAction, @@ -28,8 +34,13 @@ import type { CloseIngredientSelectorAction, DrillDownOnLabwareAction, DrillUpFromLabwareAction, + SelectLabwareAction, + SelectNestedLabwareAction, + SelectModuleAction, + SelectFixtureAction, + ZoomedIntoSlotAction, + GenerateNewProtocolAction, } from '../actions' -import type { LoadLabwareCreateCommand } from '@opentrons/shared-data' // REDUCERS // modeLabwareSelection: boolean. If true, we're selecting labware to add to a slot // (this state just toggles a modal) @@ -344,7 +355,75 @@ export const ingredLocations: Reducer = handleActions( }, {} ) + +const selectedSlotInfoInitialState: ZoomedIntoSlotInfoState = { + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedModuleModel: null, + selectedFixture: null, + selectedSlot: { slot: null, cutout: null }, +} + +export const zoomedInSlotInfo = ( + state: ZoomedIntoSlotInfoState = selectedSlotInfoInitialState, + action: + | SelectLabwareAction + | SelectNestedLabwareAction + | SelectModuleAction + | SelectFixtureAction + | ZoomedIntoSlotAction +): ZoomedIntoSlotInfoState => { + switch (action.type) { + case 'SELECT_LABWARE': { + const { labwareDefUri } = action.payload + return { ...state, selectedLabwareDefUri: labwareDefUri } + } + case 'SELECT_NESTED_LABWARE': { + const { nestedLabwareDefUri } = action.payload + return { ...state, selectedNestedLabwareDefUri: nestedLabwareDefUri } + } + case 'SELECT_MODULE': { + const { moduleModel } = action.payload + return { ...state, selectedModuleModel: moduleModel } + } + case 'SELECT_FIXTURE': { + const { fixture } = action.payload + return { ...state, selectedFixture: fixture } + } + case 'ZOOMED_INTO_SLOT': { + const { slot, cutout } = action.payload + return { + ...state, + selectedSlot: { + slot, + cutout, + }, + } + } + default: + return state + } +} + +const initialGenerateNewProtocolState: GenerateNewProtocolState = { + isNewProtocol: false, +} + +export const generateNewProtocol = ( + state: GenerateNewProtocolState = initialGenerateNewProtocolState, + action: GenerateNewProtocolAction +): GenerateNewProtocolState => { + switch (action.type) { + case 'GENERATE_NEW_PROTOCOL': { + const { isNewProtocol } = action.payload + return { ...state, isNewProtocol } + } + default: + return state + } +} export interface RootState { + zoomedInSlotInfo: ZoomedIntoSlotInfoState modeLabwareSelection: DeckSlot | false selectedContainerId: SelectedContainerId drillDownLabwareId: DrillDownLabwareId @@ -353,9 +432,11 @@ export interface RootState { selectedLiquidGroup: SelectedLiquidGroupState ingredients: IngredientsState ingredLocations: LocationsState + generateNewProtocol: GenerateNewProtocolState } // TODO Ian 2018-01-15 factor into separate files export const rootReducer: Reducer = combineReducers({ + zoomedInSlotInfo, modeLabwareSelection, selectedContainerId, selectedLiquidGroup, @@ -364,4 +445,5 @@ export const rootReducer: Reducer = combineReducers({ savedLabware, ingredients, ingredLocations, + generateNewProtocol, }) diff --git a/protocol-designer/src/labware-ingred/selectors.ts b/protocol-designer/src/labware-ingred/selectors.ts index 817b2c705e5..eb7767af225 100644 --- a/protocol-designer/src/labware-ingred/selectors.ts +++ b/protocol-designer/src/labware-ingred/selectors.ts @@ -6,6 +6,7 @@ import reduce from 'lodash/reduce' import type { Selector } from 'reselect' import type { Options } from '@opentrons/components' import type { LabwareLiquidState } from '@opentrons/step-generation' +import type { CutoutId } from '@opentrons/shared-data' import type { RootState, ContainersState, @@ -19,6 +20,7 @@ import type { IngredInputs, LiquidGroup, OrderedLiquids, + ZoomedIntoSlotInfoState, } from './types' import type { BaseState, DeckSlot } from './../types' // TODO: Ian 2019-02-15 no RootSlice, use BaseState @@ -157,6 +159,25 @@ const getLiquidDisplayColors: Selector = createSelector( return acc }, []) ) + +const getZoomedInSlotInfo: Selector< + RootSlice, + ZoomedIntoSlotInfoState +> = createSelector(rootSelector, rootState => rootState.zoomedInSlotInfo) + +const getZoomedInSlot: Selector< + RootSlice, + { slot: DeckSlot | null; cutout: CutoutId | null } +> = createSelector( + rootSelector, + rootState => rootState.zoomedInSlotInfo.selectedSlot +) + +const getIsNewProtocol: Selector = createSelector( + rootSelector, + rootState => rootState.generateNewProtocol.isNewProtocol +) + // TODO: prune selectors export const selectors = { rootSelector, @@ -177,4 +198,7 @@ export const selectors = { selectedAddLabwareSlot, getDeckHasLiquid, getLiquidDisplayColors, + getZoomedInSlotInfo, + getZoomedInSlot, + getIsNewProtocol, } diff --git a/protocol-designer/src/labware-ingred/types.ts b/protocol-designer/src/labware-ingred/types.ts index 02bb8afa6ad..6e9567722f3 100644 --- a/protocol-designer/src/labware-ingred/types.ts +++ b/protocol-designer/src/labware-ingred/types.ts @@ -1,4 +1,5 @@ -import type { LocationLiquidState } from '@opentrons/step-generation' +import type { CutoutId, ModuleModel } from '@opentrons/shared-data' +import type { DeckSlot, LocationLiquidState } from '@opentrons/step-generation' // TODO Ian 2018-02-19 make these shared in component library, standardize with Run App // ===== LABWARE =========== export interface DisplayLabware { @@ -43,3 +44,21 @@ export type IngredInputs = LiquidGroup & { export type IngredGroupAccessor = keyof IngredInputs export type LiquidGroupsById = Record export type AllIngredGroupFields = Record + +export type Fixture = + | 'stagingArea' + | 'trashBin' + | 'wasteChute' + | 'wasteChuteAndStagingArea' + +export interface ZoomedIntoSlotInfoState { + selectedLabwareDefUri: string | null + selectedNestedLabwareDefUri: string | null + selectedModuleModel: ModuleModel | null + selectedFixture: Fixture | null + selectedSlot: { slot: DeckSlot | null; cutout: CutoutId | null } +} + +export interface GenerateNewProtocolState { + isNewProtocol: boolean +} diff --git a/protocol-designer/src/molecules/SettingsIcon/__tests__/SettingsIcon.test.tsx b/protocol-designer/src/molecules/SettingsIcon/__tests__/SettingsIcon.test.tsx new file mode 100644 index 00000000000..5ec0f40d48d --- /dev/null +++ b/protocol-designer/src/molecules/SettingsIcon/__tests__/SettingsIcon.test.tsx @@ -0,0 +1,38 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach, expect } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../__testing-utils__' +import { getFileMetadata } from '../../../file-data/selectors' +import { SettingsIcon } from '..' +import type { NavigateFunction } from 'react-router-dom' + +const mockNavigate = vi.fn() + +vi.mock('react-router-dom', async importOriginal => { + const reactRouterDom = await importOriginal() + return { + ...reactRouterDom, + useNavigate: () => mockNavigate, + useLocation: () => ({ + location: { + pathname: '/settings', + }, + }), + } +}) +vi.mock('../../../file-data/selectors') + +const render = () => { + return renderWithProviders()[0] +} + +describe('SettingsIcon', () => { + beforeEach(() => { + vi.mocked(getFileMetadata).mockReturnValue({}) + }) + it('renders the SettingsIcon', () => { + render() + fireEvent.click(screen.getByTestId('SettingsIconButton')) + expect(mockNavigate).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/molecules/SettingsIcon/index.tsx b/protocol-designer/src/molecules/SettingsIcon/index.tsx new file mode 100644 index 00000000000..accf1fa5720 --- /dev/null +++ b/protocol-designer/src/molecules/SettingsIcon/index.tsx @@ -0,0 +1,54 @@ +import * as React from 'react' +import { useSelector } from 'react-redux' +import { useLocation, useNavigate } from 'react-router-dom' +import { + BORDERS, + Btn, + COLORS, + Flex, + Icon, + JUSTIFY_CENTER, +} from '@opentrons/components' +import { getFileMetadata } from '../../file-data/selectors' +import { BUTTON_LINK_STYLE } from '../../atoms/constants' + +// TODO(ja): this icon needs to be updated to match css states and correct svg +export const SettingsIcon = (): JSX.Element => { + const location = useLocation() + const navigate = useNavigate() + const metadata = useSelector(getFileMetadata) + + const handleNavigate = (): void => { + if (metadata?.created != null && location.pathname === '/settings') { + navigate(-1) + } else if (location.pathname !== '/settings') { + navigate('/settings') + } else { + navigate('/') + } + } + + return ( + + + + + + + + ) +} diff --git a/protocol-designer/src/molecules/index.ts b/protocol-designer/src/molecules/index.ts index a865b38e935..ae458bce67b 100644 --- a/protocol-designer/src/molecules/index.ts +++ b/protocol-designer/src/molecules/index.ts @@ -1 +1 @@ -console.log('molecules for new components') +export * from './SettingsIcon' diff --git a/protocol-designer/src/components/modals/AnnouncementModal/AnnouncementModal.module.css b/protocol-designer/src/organisms/AnnouncementModal/AnnouncementModal.module.css similarity index 100% rename from protocol-designer/src/components/modals/AnnouncementModal/AnnouncementModal.module.css rename to protocol-designer/src/organisms/AnnouncementModal/AnnouncementModal.module.css diff --git a/protocol-designer/src/components/modals/AnnouncementModal/__tests__/AnnouncementModal.test.tsx b/protocol-designer/src/organisms/AnnouncementModal/__tests__/AnnouncementModal.test.tsx similarity index 73% rename from protocol-designer/src/components/modals/AnnouncementModal/__tests__/AnnouncementModal.test.tsx rename to protocol-designer/src/organisms/AnnouncementModal/__tests__/AnnouncementModal.test.tsx index 8c1443d55ca..2b5e693424b 100644 --- a/protocol-designer/src/components/modals/AnnouncementModal/__tests__/AnnouncementModal.test.tsx +++ b/protocol-designer/src/organisms/AnnouncementModal/__tests__/AnnouncementModal.test.tsx @@ -1,14 +1,13 @@ import * as React from 'react' -import '@testing-library/jest-dom/vitest' -import { fireEvent, screen, cleanup } from '@testing-library/react' -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { renderWithProviders } from '../../../../__testing-utils__' -import { i18n } from '../../../../assets/localization' -import { getLocalStorageItem, setLocalStorageItem } from '../../../../persist' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../assets/localization' +import { getLocalStorageItem, setLocalStorageItem } from '../../../persist' import { useAnnouncements } from '../announcements' import { AnnouncementModal } from '../index' -vi.mock('../../../../persist') +vi.mock('../../../persist') vi.mock('../announcements') const render = () => { @@ -27,9 +26,6 @@ describe('AnnouncementModal', () => { }, ]) }) - afterEach(() => { - cleanup() - }) it('renders an announcement modal that has not been seen', () => { render() screen.getByText('mockMessage') diff --git a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx b/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx similarity index 81% rename from protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx rename to protocol-designer/src/organisms/AnnouncementModal/announcements.tsx index dd9e5f93b81..68c689cc31a 100644 --- a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx +++ b/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx @@ -2,21 +2,23 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' import { + DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, JUSTIFY_SPACE_AROUND, SPACING, + StyledText, } from '@opentrons/components' -import magTempCombined from '../../../assets/images/modules/magdeck_tempdeck_combined.png' -import thermocycler from '../../../assets/images/modules/thermocycler.png' -import multiSelect from '../../../assets/images/announcements/multi_select.gif' -import batchEdit from '../../../assets/images/announcements/batch_edit.gif' -import heaterShaker from '../../../assets/images/modules/heatershaker.png' -import thermocyclerGen2 from '../../../assets/images/modules/thermocycler_gen2.png' -import liquidEnhancements from '../../../assets/images/announcements/liquid-enhancements.gif' -import opentronsFlex from '../../../assets/images/OpentronsFlex.png' -import deckConfigutation from '../../../assets/images/deck_configuration.png' +import magTempCombined from '../../assets/images/modules/magdeck_tempdeck_combined.png' +import thermocycler from '../../assets/images/modules/thermocycler.png' +import multiSelect from '../../assets/images/announcements/multi_select.gif' +import batchEdit from '../../assets/images/announcements/batch_edit.gif' +import heaterShaker from '../../assets/images/modules/heatershaker.png' +import thermocyclerGen2 from '../../assets/images/modules/thermocycler_gen2.png' +import liquidEnhancements from '../../assets/images/announcements/liquid-enhancements.gif' +import opentronsFlex from '../../assets/images/OpentronsFlex.png' +import deckConfigutation from '../../assets/images/deck_configuration.png' import styles from './AnnouncementModal.module.css' @@ -299,5 +301,39 @@ export const useAnnouncements = (): Announcement[] => { ), }, + { + announcementKey: 'redesign9.0', + image: , + heading: t('announcements.redesign.body1'), + message: ( + + + {t('announcements.redesign.body2')} + + +
    +
  • + + {t('announcements.redesign.body3')} + +
  • + +
  • + + {t('announcements.redesign.body4')} + +
  • +
+
+ + }} + i18nKey={'announcements.redesign.body5'} + /> + +
+ ), + }, ] } diff --git a/protocol-designer/src/components/modals/AnnouncementModal/index.tsx b/protocol-designer/src/organisms/AnnouncementModal/index.tsx similarity index 51% rename from protocol-designer/src/components/modals/AnnouncementModal/index.tsx rename to protocol-designer/src/organisms/AnnouncementModal/index.tsx index 4f461e9749e..ec40fe5bd28 100644 --- a/protocol-designer/src/components/modals/AnnouncementModal/index.tsx +++ b/protocol-designer/src/organisms/AnnouncementModal/index.tsx @@ -1,15 +1,20 @@ import * as React from 'react' -import cx from 'classnames' import { useTranslation } from 'react-i18next' -import { LegacyModal, OutlineButton } from '@opentrons/components' +import { + DIRECTION_COLUMN, + Flex, + JUSTIFY_CENTER, + JUSTIFY_END, + Modal, + PrimaryButton, + SPACING, +} from '@opentrons/components' import { setLocalStorageItem, getLocalStorageItem, localStorageAnnouncementKey, -} from '../../../persist' +} from '../../persist' import { useAnnouncements } from './announcements' -import modalStyles from '../modal.module.css' -import styles from './AnnouncementModal.module.css' export const AnnouncementModal = (): JSX.Element => { const { t } = useTranslation(['modal', 'button']) @@ -35,29 +40,26 @@ export const AnnouncementModal = (): JSX.Element => { return ( <> {showAnnouncementModal && ( - - {image && ( - <> - {image} -
- - )} - -
-

{heading}

-
{message}
- -
- + + {t('button:got_it')} - -
-
-
+ +
+ } + > + + {image && image} + {message} + + )} ) diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx new file mode 100644 index 00000000000..321213fe1f9 --- /dev/null +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx @@ -0,0 +1,163 @@ +import * as React from 'react' +import { useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { + ALIGN_CENTER, + BORDERS, + COLORS, + DIRECTION_COLUMN, + Divider, + Flex, + Icon, + LiquidIcon, + ListItem, + SPACING, + StyledText, +} from '@opentrons/components' + +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import { getLabwareEntities } from '../../step-forms/selectors' +import * as wellContentsSelectors from '../../top-selectors/well-contents' + +import type { LiquidInfo } from './LiquidToolbox' + +interface LiquidCardProps { + info: LiquidInfo +} + +export function LiquidCard(props: LiquidCardProps): JSX.Element { + const { info } = props + const { name, color, liquidIndex } = info + const { t } = useTranslation('liquids') + const [isExpanded, setIsExpanded] = React.useState(false) + const labwareId = useSelector(labwareIngredSelectors.getSelectedLabwareId) + const labwareEntities = useSelector(getLabwareEntities) + const selectedLabwareDef = + labwareId != null ? labwareEntities[labwareId] : null + const liquidsWithDescriptions = useSelector( + labwareIngredSelectors.allIngredientGroupFields + ) + const orderedWells = selectedLabwareDef?.def.ordering.flat() ?? [] + const allWellContentsForActiveItem = useSelector( + wellContentsSelectors.getAllWellContentsForActiveItem + ) + const wellContents = + allWellContentsForActiveItem != null && labwareId != null + ? allWellContentsForActiveItem[labwareId] + : null + const liquidsInLabware = + wellContents != null + ? Object.values(wellContents).flatMap(content => content.groupIds) + : null + const uniqueLiquids = Array.from(new Set(liquidsInLabware)) + + const fullWellsByLiquid = uniqueLiquids.reduce>( + (acc, liq) => { + if (allWellContentsForActiveItem != null && labwareId != null) { + const wellContents = allWellContentsForActiveItem[labwareId] ?? {} + Object.entries(wellContents).forEach(([wellName, well]) => { + const { groupIds } = well + if (groupIds.includes(liq)) { + if (liq in acc) { + acc[liq] = [...acc[liq], wellName] + } else { + acc[liq] = [wellName] + } + } + }) + } + return acc + }, + {} + ) + + return ( + + + + + {name} + + {info.liquidIndex != null + ? liquidsWithDescriptions[info.liquidIndex].description + : null} + + + { + setIsExpanded(!isExpanded) + }} + > + + + + {isExpanded ? ( + + + + {t('well')} + + + + {t('microliters')} + + + + + {info.liquidIndex != null + ? fullWellsByLiquid[info.liquidIndex] + .sort((a, b) => + orderedWells.indexOf(b) > orderedWells.indexOf(a) ? -1 : 1 + ) + .map((wellName, wellliquidIndex) => { + const volume = + wellContents != null + ? wellContents[wellName].ingreds[liquidIndex].volume + : 0 + return ( + <> + + {wellliquidIndex < + fullWellsByLiquid[liquidIndex].length - 1 ? ( + + ) : null} + + ) + }) + : null} + + ) : null} + + ) +} + +interface WellContentsProps { + wellName: string + volume: number +} + +function WellContents(props: WellContentsProps): JSX.Element { + const { wellName, volume } = props + const { t } = useTranslation('liquids') + + return ( + + + {wellName} + + + {`${volume} ${t('microliters')}`} + + + ) +} diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx new file mode 100644 index 00000000000..997bdfa849b --- /dev/null +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx @@ -0,0 +1,354 @@ +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { Controller, useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { + Btn, + DIRECTION_COLUMN, + DropdownMenu, + Flex, + InputField, + JUSTIFY_SPACE_BETWEEN, + ListItem, + PrimaryButton, + SPACING, + StyledText, + TYPOGRAPHY, + Toolbox, +} from '@opentrons/components' +import * as wellContentsSelectors from '../../top-selectors/well-contents' +import * as fieldProcessors from '../../steplist/fieldLevel/processing' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import { getSelectedWells } from '../../well-selection/selectors' +import { getLabwareNicknamesById } from '../../ui/labware/selectors' +import { + removeWellsContents, + setWellContents, +} from '../../labware-ingred/actions' +import { deselectAllWells } from '../../well-selection/actions' +import { LiquidCard } from './LiquidCard' + +import type { DropdownOption } from '@opentrons/components' +import type { ContentsByWell } from '../../labware-ingred/types' + +export interface LiquidInfo { + name: string + color: string + liquidIndex: string +} + +interface ValidFormValues { + selectedLiquidId: string + volume: string +} + +interface ToolboxFormValues { + selectedLiquidId?: string | null + volume?: string | null +} +interface LiquidToolboxProps { + onClose: () => void +} +export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { + const { onClose } = props + const { t } = useTranslation(['liquids', 'shared']) + const dispatch = useDispatch() + const liquids = useSelector(labwareIngredSelectors.allIngredientNamesIds) + const labwareId = useSelector(labwareIngredSelectors.getSelectedLabwareId) + const selectedWellGroups = useSelector(getSelectedWells) + const nickNames = useSelector(getLabwareNicknamesById) + const selectedWells = Object.keys(selectedWellGroups) + const labwareDisplayName = labwareId != null ? nickNames[labwareId] : '' + const liquidLocations = useSelector( + labwareIngredSelectors.getLiquidsByLabwareId + ) + const commonSelectedLiquidId = useSelector( + wellContentsSelectors.getSelectedWellsCommonIngredId + ) + const commonSelectedVolume = useSelector( + wellContentsSelectors.getSelectedWellsCommonVolume + ) + const selectedWellsMaxVolume = useSelector( + wellContentsSelectors.getSelectedWellsMaxVolume + ) + const liquidSelectionOptions = useSelector( + labwareIngredSelectors.getLiquidSelectionOptions + ) + const allWellContentsForActiveItem = useSelector( + wellContentsSelectors.getAllWellContentsForActiveItem + ) + + const selectionHasLiquids = Boolean( + labwareId != null && + liquidLocations[labwareId] != null && + Object.keys(selectedWellGroups).some( + well => liquidLocations[labwareId][well] + ) + ) + + const getInitialValues: () => ValidFormValues = () => { + return { + selectedLiquidId: commonSelectedLiquidId ?? '', + volume: + commonSelectedVolume != null ? commonSelectedVolume.toString() : '', + } + } + + const { + handleSubmit, + watch, + control, + setValue, + reset, + formState: { touchedFields }, + } = useForm({ + defaultValues: getInitialValues(), + }) + + const selectedLiquidId = watch('selectedLiquidId') + const volume = watch('volume') + + const handleCancelForm = (): void => { + dispatch(deselectAllWells()) + } + + const handleClearSelectedWells: () => void = () => { + if (labwareId != null && selectedWells != null && selectionHasLiquids) { + if (global.confirm(t('application:are_you_sure') as string)) { + dispatch( + removeWellsContents({ + labwareId, + wells: selectedWells, + }) + ) + } + } + } + + const handleChangeVolume: ( + e: React.ChangeEvent + ) => void = e => { + const value: string | null | undefined = e.currentTarget.value + const masked = fieldProcessors.composeMaskers( + fieldProcessors.maskToFloat, + fieldProcessors.onlyPositiveNumbers, + fieldProcessors.trimDecimals(1) + )(value) as string + setValue('volume', masked) + } + + const handleSaveForm = (values: ToolboxFormValues): void => { + const volume = Number(values.volume) + const { selectedLiquidId } = values + console.assert( + labwareId != null, + 'when saving liquid placement form, expected a selected labware ID' + ) + console.assert( + selectedWells != null && selectedWells.length > 0, + `when saving liquid placement form, expected selected wells to be array with length > 0 but got ${String( + selectedWells + )}` + ) + console.assert( + selectedLiquidId != null, + `when saving liquid placement form, expected selectedLiquidId to be non-nullsy but got ${String( + selectedLiquidId + )}` + ) + console.assert( + volume > 0, + `when saving liquid placement form, expected volume > 0, got ${volume}` + ) + + if (labwareId != null && selectedLiquidId != null) { + dispatch( + setWellContents({ + liquidGroupId: selectedLiquidId, + labwareId, + wells: selectedWells ?? [], + volume: Number(values.volume), + }) + ) + } + } + + const handleSaveSubmit: (values: ToolboxFormValues) => void = values => { + handleSaveForm(values) + reset() + } + + let volumeErrors: string | null = null + if (Boolean(touchedFields.volume)) { + if (volume == null || volume === '0') { + volumeErrors = t('generic.error.more_than_zero') + } else if (parseInt(volume) > selectedWellsMaxVolume) { + volumeErrors = t('liquid_placement.volume_exceeded', { + volume: selectedWellsMaxVolume, + }) + } + } + + let wellContents: ContentsByWell | null = null + if (allWellContentsForActiveItem != null && labwareId != null) { + wellContents = allWellContentsForActiveItem[labwareId] + } + + const liquidsInLabware = + wellContents != null + ? Object.values(wellContents).flatMap(content => content.groupIds) + : null + + const uniqueLiquids = Array.from(new Set(liquidsInLabware)) + + const liquidInfo: LiquidInfo[] = uniqueLiquids + .map(liquid => { + const foundLiquid = Object.values(liquids).find( + id => id.ingredientId === liquid + ) + return { + liquidIndex: liquid, + name: foundLiquid?.name ?? '', + color: foundLiquid?.displayColor ?? '', + } + }) + .filter(Boolean) + return ( + + {labwareDisplayName} + + } + confirmButtonText={t('shared:done')} + onConfirmClick={onClose} + onCloseClick={handleClearSelectedWells} + height="calc(100vh - 64px)" + closeButtonText={t('clear_wells')} + disableCloseButton={ + !(labwareId != null && selectedWells != null && selectionHasLiquids) + } + > +
+ + {selectedWells.length > 0 ? ( + + + {t('add_liquid')} + + + + {t('liquid')} + + { + const fullOptions: DropdownOption[] = liquidSelectionOptions.map( + option => { + const liquid = liquids.find( + liquid => liquid.ingredientId === option.value + ) + + return { + name: option.name, + value: option.value, + liquidColor: liquid?.displayColor ?? '', + } + } + ) + const selectedLiquid = fullOptions.find( + option => option.value === selectedLiquidId + ) + const selectLiquidIdName = selectedLiquid?.name + const selectLiquidColor = selectedLiquid?.liquidColor + + return ( + + ) + }} + /> + + + + + {t('liquid_volume')} + + ( + + )} + /> + + + + + {t('shared:cancel')} + + + + {t('save')} + + + + ) : null} + + + {liquidInfo.length > 0 ? ( + + {t('liquids_added')} + + ) : null} + {liquidInfo.map(info => { + return + })} + +
+
+ ) +} diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx new file mode 100644 index 00000000000..6db4309a9f7 --- /dev/null +++ b/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx @@ -0,0 +1,114 @@ +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { + ALIGN_CENTER, + BORDERS, + Box, + COLORS, + Flex, + JUSTIFY_CENTER, + JUSTIFY_SPACE_BETWEEN, + SPACING, + StyledText, + WELL_LABEL_OPTIONS, +} from '@opentrons/components' +import { selectors } from '../../labware-ingred/selectors' +import { selectors as stepFormSelectors } from '../../step-forms' +import * as wellContentsSelectors from '../../top-selectors/well-contents' +import { getSelectedWells } from '../../well-selection/selectors' +import { + SelectableLabware, + wellFillFromWellContents, +} from '../../components/labware' +import { deselectWells, selectWells } from '../../well-selection/actions' +import { LiquidToolbox } from './LiquidToolbox' + +import type { WellGroup } from '@opentrons/components' + +export function AssignLiquidsModal(): JSX.Element | null { + const { t } = useTranslation('liquids') + const [highlightedWells, setHighlightedWells] = React.useState< + WellGroup | {} + >({}) + const navigate = useNavigate() + const labwareId = useSelector(selectors.getSelectedLabwareId) + const selectedWells = useSelector(getSelectedWells) + const dispatch = useDispatch() + const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities) + const allWellContents = useSelector( + wellContentsSelectors.getWellContentsAllLabware + ) + const liquidNamesById = useSelector(selectors.getLiquidNamesById) + const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors) + if (labwareId == null) { + console.assert( + false, + 'No labware is selected, and no labwareId was given to AssignLiquidsModal' + ) + return null + } + + const labwareDef = labwareEntities[labwareId]?.def + const wellContents = allWellContents[labwareId] + + return ( + + + + + + {t('click_and_drag')} + + + dispatch(selectWells(wells))} + deselectWells={(wells: WellGroup) => dispatch(deselectWells(wells))} + updateHighlightedWells={(wells: WellGroup) => { + setHighlightedWells(wells) + }} + ingredNames={liquidNamesById} + wellContents={wellContents} + nozzleType={null} + /> + + + { + navigate('/designer') + }} + /> + + ) +} diff --git a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx new file mode 100644 index 00000000000..f20f9a10072 --- /dev/null +++ b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx @@ -0,0 +1,316 @@ +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { SketchPicker } from 'react-color' +import { yupResolver } from '@hookform/resolvers/yup' +import styled from 'styled-components' +import * as Yup from 'yup' +import { Controller, useForm } from 'react-hook-form' +import { + DEFAULT_LIQUID_COLORS, + DEPRECATED_WHALE_GREY, +} from '@opentrons/shared-data' +import { + BORDERS, + Btn, + COLORS, + DIRECTION_COLUMN, + Flex, + InputField, + JUSTIFY_END, + JUSTIFY_SPACE_BETWEEN, + LiquidIcon, + Modal, + POSITION_ABSOLUTE, + PrimaryButton, + SecondaryButton, + SPACING, + StyledText, + TYPOGRAPHY, + useOnClickOutside, +} from '@opentrons/components' +import * as labwareIngredActions from '../../labware-ingred/actions' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import { swatchColors } from '../../components/swatchColors' +import { checkColor } from './utils' + +import type { ColorResult } from 'react-color' +import type { ThunkDispatch } from 'redux-thunk' +import type { BaseState } from '../../types' +import type { LiquidGroup } from '../../labware-ingred/types' + +interface LiquidEditFormValues { + name: string + displayColor: string + description?: string | null + serialize?: boolean + [key: string]: unknown +} + +const INVALID_DISPLAY_COLORS = ['#000000', '#ffffff', DEPRECATED_WHALE_GREY] + +const liquidEditFormSchema: any = Yup.object().shape({ + name: Yup.string().required('liquid name is required'), + displayColor: Yup.string().test( + 'disallowed-color', + 'Invalid display color', + value => { + if (value == null) { + return true + } + return !INVALID_DISPLAY_COLORS.includes(value) + ? !checkColor(value) + : false + } + ), + description: Yup.string(), + serialize: Yup.boolean(), +}) + +interface DefineLiquidsModalProps { + onClose: () => void +} +export function DefineLiquidsModal( + props: DefineLiquidsModalProps +): JSX.Element { + const { onClose } = props + const dispatch = useDispatch>() + const { t } = useTranslation(['liquids', 'shared']) + const selectedLiquid = useSelector( + labwareIngredSelectors.getSelectedLiquidGroupState + ) + const nextGroupId = useSelector(labwareIngredSelectors.getNextLiquidGroupId) + const selectedLiquidGroupState = useSelector( + labwareIngredSelectors.getSelectedLiquidGroupState + ) + const [showColorPicker, setShowColorPicker] = React.useState(false) + const chooseColorWrapperRef = useOnClickOutside({ + onClickOutside: () => { + setShowColorPicker(false) + }, + }) + const allIngredientGroupFields = useSelector( + labwareIngredSelectors.allIngredientGroupFields + ) + const liquidGroupId = + selectedLiquidGroupState && selectedLiquidGroupState.liquidGroupId + const deleteLiquidGroup = (): void => { + if (liquidGroupId != null) { + dispatch(labwareIngredActions.deleteLiquidGroup(liquidGroupId)) + } + onClose() + } + + const cancelForm = (): void => { + dispatch(labwareIngredActions.deselectLiquidGroup()) + onClose() + } + + const saveForm = (formData: LiquidGroup): void => { + dispatch( + labwareIngredActions.editLiquidGroup({ + ...formData, + liquidGroupId: liquidGroupId, + }) + ) + onClose() + } + + const selectedIngredFields = + liquidGroupId != null ? allIngredientGroupFields[liquidGroupId] : null + const liquidId = selectedLiquid.liquidGroupId ?? nextGroupId + + const initialValues: LiquidEditFormValues = { + name: selectedIngredFields?.name ?? '', + displayColor: selectedIngredFields?.displayColor ?? swatchColors(liquidId), + description: selectedIngredFields?.description ?? '', + serialize: selectedIngredFields?.serialize ?? false, + } + + const { + handleSubmit, + formState: { errors, touchedFields }, + control, + watch, + setValue, + register, + } = useForm({ + defaultValues: initialValues, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + resolver: yupResolver(liquidEditFormSchema), + }) + const name = watch('name') + const color = watch('displayColor') + + const handleLiquidEdits = (values: LiquidEditFormValues): void => { + saveForm({ + name: values.name, + displayColor: values.displayColor, + description: values.description ?? null, + serialize: values.serialize ?? false, + }) + } + + return ( + + + + {initialValues.name} + + + ) : ( + t('define_liquid') + ) + } + type="info" + onClose={onClose} + > +
+ <> + {showColorPicker ? ( + + ( + { + setValue('displayColor', color.hex) + + field.onChange(color.hex) + }} + /> + )} + /> + + ) : null} + + + + + + {t('name')} + + ( + + )} + /> + + + + {t('description')} + + + + + + {t('display_color')} + + + { + setShowColorPicker(prev => !prev) + }} + color={color} + size="medium" + /> + + {/* NOTE: this is for serialization if we decide to add it back */} + {/* ( + ) => { + field.onChange(e) + }} + /> + )} + /> */} + + + {selectedIngredFields != null ? ( + + + {t('delete_liquid')} + + + ) : ( + + {t('shared:close')} + + )} + + {t('shared:save')} + + + + +
+
+ ) +} + +const DescriptionField = styled.textarea` + height: 6.875rem; + width: 100%; + border: 1px solid ${COLORS.grey50}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeP}; + resize: none; +` diff --git a/protocol-designer/src/organisms/DefineLiquidsModal/utils.ts b/protocol-designer/src/organisms/DefineLiquidsModal/utils.ts new file mode 100644 index 00000000000..d55cbb4cf5b --- /dev/null +++ b/protocol-designer/src/organisms/DefineLiquidsModal/utils.ts @@ -0,0 +1,8 @@ +export function checkColor(hex: string): boolean { + const cleanHex = hex.replace('#', '') + const red = parseInt(cleanHex.slice(0, 2), 16) + const green = parseInt(cleanHex.slice(2, 4), 16) + const blue = parseInt(cleanHex.slice(4, 6), 16) + const luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255 + return luminance < 0.1 || luminance > 0.9 +} diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/editPipettes.ts b/protocol-designer/src/organisms/EditInstrumentsModal/editPipettes.ts new file mode 100644 index 00000000000..a92e6717d33 --- /dev/null +++ b/protocol-designer/src/organisms/EditInstrumentsModal/editPipettes.ts @@ -0,0 +1,217 @@ +import filter from 'lodash/filter' +import isEmpty from 'lodash/isEmpty' +import last from 'lodash/last' +import mapValues from 'lodash/mapValues' + +import { actions as stepFormActions } from '../../step-forms' +import { actions as steplistActions } from '../../steplist' +import { uuid } from '../../utils' +import { createContainer, deleteContainer } from '../../labware-ingred/actions' +import { INITIAL_DECK_SETUP_STEP_ID } from '../../constants' + +import type { PipetteMount, PipetteName } from '@opentrons/shared-data' +import type { NormalizedPipette } from '@opentrons/step-generation' +import type { ThunkDispatch } from '../../types' +import type { LabwareOnDeck, PipetteOnDeck } from '../../step-forms' +import type { StepIdType } from '../../form-types' + +const adapter96ChannelDefUri = 'opentrons/opentrons_flex_96_tiprack_adapter/1' + +type PipetteFieldsData = Omit< + PipetteOnDeck, + 'id' | 'spec' | 'tiprackLabwareDef' +> + +export const editPipettes = ( + labware: { [labwareId: string]: LabwareOnDeck }, + pipettes: { [pipetteId: string]: PipetteOnDeck }, + orderedStepIds: StepIdType[], + dispatch: ThunkDispatch, + mount: PipetteMount, + selectedPip: PipetteName, + selectedTips: string[], + leftPip?: PipetteOnDeck, + rightPip?: PipetteOnDeck +): void => { + const oppositePipette = mount === 'left' ? rightPip : leftPip + const otherPipFields: PipetteFieldsData | null = + oppositePipette != null + ? { + mount: oppositePipette.mount, + name: oppositePipette.name, + tiprackDefURI: oppositePipette.tiprackDefURI, + } + : null + const newPip: PipetteFieldsData = { + mount: mount, + name: selectedPip, + tiprackDefURI: selectedTips, + } + + const newPipetteArray: PipetteFieldsData[] = + otherPipFields != null ? [otherPipFields, newPip] : [newPip] + + const prevPipetteIds = Object.keys(pipettes) + const usedPrevPipettes: string[] = [] + const nextPipettes: { + [pipetteId: string]: { + mount: string + name: PipetteName + tiprackDefURI: string[] + id: string + } + } = {} + + newPipetteArray.forEach((newPipette: PipetteFieldsData) => { + const candidatePipetteIds = prevPipetteIds.filter(id => { + const prevPipette = pipettes[id] + const alreadyUsed = usedPrevPipettes.some(usedId => usedId === id) + return !alreadyUsed && prevPipette.name === newPipette.name + }) + const pipetteId: string | null | undefined = candidatePipetteIds[0] + if (pipetteId) { + // update used pipette list + usedPrevPipettes.push(pipetteId) + nextPipettes[pipetteId] = { ...newPipette, id: pipetteId } + } else { + const newId = uuid() + nextPipettes[newId] = { ...newPipette, id: newId } + } + }) + + const newTiprackUris = new Set( + newPipetteArray.flatMap(pipette => pipette.tiprackDefURI) + ) + const previousTiprackLabwares = Object.values(labware).filter( + lw => lw.def.parameters.isTiprack + ) + + const previousTiprackUris = new Set( + previousTiprackLabwares.map(labware => labware.labwareDefURI) + ) + + // Find tipracks to delete (old tipracks not in new pipettes) + previousTiprackLabwares + .filter(labware => !newTiprackUris.has(labware.labwareDefURI)) + .forEach(labware => dispatch(deleteContainer({ labwareId: labware.id }))) + + // Create new tipracks that are not in previous tiprackURIs + newTiprackUris.forEach(tiprackDefUri => { + if (!previousTiprackUris.has(tiprackDefUri)) { + const adapterUnderLabwareDefURI = newPipetteArray.some( + pipette => pipette.name === 'p1000_96' + ) + ? adapter96ChannelDefUri + : undefined + dispatch( + createContainer({ + labwareDefURI: tiprackDefUri, + adapterUnderLabwareDefURI, + }) + ) + } + }) + dispatch( + stepFormActions.createPipettes( + mapValues( + nextPipettes, + ({ + id, + name, + tiprackDefURI, + }: typeof nextPipettes[keyof typeof nextPipettes]): NormalizedPipette => ({ + id, + name, + tiprackDefURI, + }) + ) + ) + ) + + // set/update pipette locations in initial deck setup step + dispatch( + steplistActions.changeSavedStepForm({ + stepId: INITIAL_DECK_SETUP_STEP_ID, + update: { + pipetteLocationUpdate: mapValues( + nextPipettes, + (p: PipetteOnDeck) => p.mount + ), + }, + }) + ) + + const pipetteIdsToDelete: string[] = Object.keys(pipettes).filter( + id => !(id in nextPipettes) + ) + + // SubstitutionMap represents a map of oldPipetteId => newPipetteId + // When a pipette's tiprack changes, the ids will be the same + interface SubstitutionMap { + [pipetteId: string]: string + } + + const pipetteReplacementMap: SubstitutionMap = pipetteIdsToDelete.reduce( + (acc: SubstitutionMap, deletedId: string): SubstitutionMap => { + const deletedPipette = pipettes[deletedId] + const replacementId = Object.keys(nextPipettes).find( + newId => nextPipettes[newId].mount === deletedPipette.mount + ) + // @ts-expect-error(sa, 2021-6-21): redlacementId will always be a string, so right side of the and will always be true + return replacementId && replacementId !== -1 + ? { ...acc, [deletedId]: replacementId } + : acc + }, + {} + ) + + const pipettesWithNewTipracks: string[] = filter( + nextPipettes, + (nextPipette: typeof nextPipettes[keyof typeof nextPipettes]) => { + const newPipetteId = nextPipette.id + const nextTips = nextPipette.tiprackDefURI + const oldTips = + newPipetteId in pipettes ? pipettes[newPipetteId].tiprackDefURI : null + const tiprackChanged = + oldTips != null && + nextTips.every((item, index) => item !== oldTips[index]) + return tiprackChanged + } + ).map(pipette => pipette.id) + + // this creates an identity map with all pipette ids that have new tipracks + // this will be used so that handleFormChange gets called even though the + // pipette id itself has not changed (only it's tiprack) + + const pipettesWithNewTiprackIdentityMap: SubstitutionMap = pipettesWithNewTipracks.reduce( + (acc: SubstitutionMap, id: string): SubstitutionMap => { + return { + ...acc, + ...{ [id]: id }, + } + }, + {} + ) + + const substitutionMap = { + ...pipetteReplacementMap, + ...pipettesWithNewTiprackIdentityMap, + } + + // substitute deleted pipettes with new pipettes on the same mount, if any + if (!isEmpty(substitutionMap) && orderedStepIds.length > 0) { + // NOTE: using start/end here is meant to future-proof this action for multi-step editing + dispatch( + stepFormActions.substituteStepFormPipettes({ + substitutionMap, + startStepId: orderedStepIds[0], + endStepId: last(orderedStepIds) ?? '', + }) + ) + } + + // delete any pipettes no longer in use + if (pipetteIdsToDelete.length > 0) { + dispatch(stepFormActions.deletePipettes(pipetteIdsToDelete)) + } +} diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx new file mode 100644 index 00000000000..fbaa2fb8954 --- /dev/null +++ b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx @@ -0,0 +1,588 @@ +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import styled, { css } from 'styled-components' +import mapValues from 'lodash/mapValues' +import { + ALIGN_CENTER, + Box, + Btn, + Checkbox, + COLORS, + DIRECTION_COLUMN, + DIRECTION_ROW, + DISPLAY_FLEX, + DISPLAY_INLINE_BLOCK, + EmptySelectorButton, + Flex, + Icon, + JUSTIFY_END, + JUSTIFY_SPACE_BETWEEN, + ListItem, + Modal, + PrimaryButton, + PRODUCT, + RadioButton, + SecondaryButton, + SPACING, + StyledText, + TYPOGRAPHY, + WRAP, +} from '@opentrons/components' +import { + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, + getAllPipetteNames, +} from '@opentrons/shared-data' +import { getTopPortalEl } from '../../components/portals/TopPortal' +import { getAllowAllTipracks } from '../../feature-flags/selectors' +import { + getAdditionalEquipment, + getInitialDeckSetup, + getPipetteEntities, +} from '../../step-forms/selectors' +import { getHas96Channel } from '../../utils' +import { changeSavedStepForm } from '../../steplist/actions' +import { INITIAL_DECK_SETUP_STEP_ID } from '../../constants' +import { PipetteInfoItem } from '../PipetteInfoItem' +import { deletePipettes } from '../../step-forms/actions' +import { toggleIsGripperRequired } from '../../step-forms/actions/additionalItems' +import { getRobotType } from '../../file-data/selectors' +import { + PIPETTE_GENS, + PIPETTE_TYPES, + PIPETTE_VOLUMES, +} from '../../pages/CreateNewProtocolWizard/constants' +import { getTiprackOptions } from '../../components/modals/utils' +import { getLabwareDefsByURI } from '../../labware-defs/selectors' +import { setFeatureFlags } from '../../feature-flags/actions' +import { createCustomTiprackDef } from '../../labware-defs/actions' +import { deleteContainer } from '../../labware-ingred/actions' +import { selectors as stepFormSelectors } from '../../step-forms' +import { getSectionsFromPipetteName } from './utils' +import { editPipettes } from './editPipettes' +import type { PipetteMount, PipetteName } from '@opentrons/shared-data' +import type { + Gen, + PipetteInfoByGen, + PipetteInfoByType, + PipetteType, +} from '../../pages/CreateNewProtocolWizard/types' +import type { ThunkDispatch } from '../../types' + +interface EditInstrumentsModalProps { + onClose: () => void +} + +export function EditInstrumentsModal( + props: EditInstrumentsModalProps +): JSX.Element { + const { onClose } = props + const dispatch = useDispatch>() + const { t } = useTranslation([ + 'create_new_protocol', + 'protocol_overview', + 'shared', + ]) + const [page, setPage] = React.useState<'add' | 'overview'>('overview') + const [mount, setMount] = React.useState('left') + const [pipetteType, setPipetteType] = React.useState(null) + const [pipetteGen, setPipetteGen] = React.useState('flex') + const [pipetteVolume, setPipetteVolume] = React.useState(null) + const [selectedTips, setSelectedTips] = React.useState([]) + const allowAllTipracks = useSelector(getAllowAllTipracks) + const robotType = useSelector(getRobotType) + const orderedStepIds = useSelector(stepFormSelectors.getOrderedStepIds) + const initialDeckSetup = useSelector(getInitialDeckSetup) + const additionalEquipment = useSelector(getAdditionalEquipment) + const pipetteEntities = useSelector(getPipetteEntities) + const allLabware = useSelector(getLabwareDefsByURI) + const allPipetteOptions = getAllPipetteNames('maxVolume', 'channels') + const { pipettes, labware } = initialDeckSetup + const pipettesOnDeck = Object.values(pipettes) + const has96Channel = getHas96Channel(pipetteEntities) + const leftPip = pipettesOnDeck.find(pip => pip.mount === 'left') + const rightPip = pipettesOnDeck.find(pip => pip.mount === 'right') + const gripper = Object.values(additionalEquipment).find( + ae => ae.name === 'gripper' + ) + const selectedPip = + pipetteType === '96' || pipetteGen === 'GEN1' + ? `${pipetteVolume}_${pipetteType}` + : `${pipetteVolume}_${pipetteType}_${pipetteGen.toLowerCase()}` + + const swapPipetteUpdate = mapValues(pipettes, pipette => { + if (!pipette.mount) return pipette.mount + return pipette.mount === 'left' ? 'right' : 'left' + }) + + const resetFields = (): void => { + setPipetteType(null) + setPipetteGen('flex') + setPipetteVolume(null) + } + + const previousLeftPipetteTipracks = Object.values(labware) + .filter(lw => lw.def.parameters.isTiprack) + .filter(tip => leftPip?.tiprackDefURI.includes(tip.labwareDefURI)) + const previousRightPipetteTipracks = Object.values(labware) + .filter(lw => lw.def.parameters.isTiprack) + .filter(tip => rightPip?.tiprackDefURI.includes(tip.labwareDefURI)) + + const rightInfo = + rightPip != null + ? getSectionsFromPipetteName(rightPip.name, rightPip.spec) + : null + const leftInfo = + leftPip != null + ? getSectionsFromPipetteName(leftPip.name, leftPip.spec) + : null + + return createPortal( + { + resetFields() + onClose() + }} + footer={ + + {page === 'overview' ? null : ( + { + setPage('overview') + resetFields() + }} + > + {t('shared:cancel')} + + )} + { + if (page === 'overview') { + onClose() + } else { + setPage('overview') + editPipettes( + labware, + pipettes, + orderedStepIds, + dispatch, + mount, + selectedPip as PipetteName, + selectedTips, + leftPip, + rightPip + ) + } + }} + > + {t(page === 'overview' ? 'shared:close' : 'shared:save')} + + + } + > + {page === 'overview' ? ( + <> + + + + {t('your_pips')} + + {has96Channel ? null : ( + + dispatch( + changeSavedStepForm({ + stepId: INITIAL_DECK_SETUP_STEP_ID, + update: { + pipetteLocationUpdate: swapPipetteUpdate, + }, + }) + ) + } + > + + + + {t('swap')} + + + + )} + + + {leftPip != null && + leftPip.tiprackDefURI != null && + leftInfo != null ? ( + { + setPage('add') + setMount('left') + setPipetteType(leftInfo.type) + setPipetteGen(leftInfo.gen) + setPipetteVolume(leftInfo.volume) + setSelectedTips(leftPip.tiprackDefURI) + }} + cleanForm={() => { + dispatch(deletePipettes([leftPip.id])) + previousLeftPipetteTipracks.forEach(tip => + dispatch(deleteContainer({ labwareId: tip.id })) + ) + }} + /> + ) : ( + { + setPage('add') + setMount('left') + resetFields() + }} + text={t('add_pip')} + textAlignment="left" + iconName="plus" + size="large" + /> + )} + {rightPip != null && + rightPip.tiprackDefURI != null && + rightInfo != null ? ( + { + setPage('add') + setMount('right') + setPipetteType(rightInfo.type) + setPipetteGen(rightInfo.gen) + setPipetteVolume(rightInfo.volume) + setSelectedTips(rightPip.tiprackDefURI) + }} + cleanForm={() => { + dispatch(deletePipettes([rightPip.id])) + previousRightPipetteTipracks.forEach(tip => + dispatch(deleteContainer({ labwareId: tip.id })) + ) + }} + /> + ) : has96Channel ? null : ( + { + setPage('add') + setMount('right') + }} + text={t('add_pip')} + textAlignment="left" + iconName="plus" + size="large" + /> + )} + + + {robotType === FLEX_ROBOT_TYPE ? ( + + + + {t('protocol_overview:your_gripper')} + + + + {gripper != null ? ( + + + + + {t('protocol_overview:extension')} + + + {t('gripper')} + + + + { + dispatch(toggleIsGripperRequired()) + }} + > + + {t('remove')} + + + + + + ) : ( + { + dispatch(toggleIsGripperRequired()) + }} + text={t('protocol_overview:add_gripper')} + textAlignment="left" + iconName="plus" + size="large" + /> + )} + + + ) : null} + + ) : ( + + <> + + {t('pip_type')} + + + {PIPETTE_TYPES[robotType].map(type => { + return type.value === '96' && has96Channel ? null : ( + { + setPipetteType(type.value) + setPipetteGen('flex') + setPipetteVolume(null) + setSelectedTips([]) + }} + buttonLabel={t(`shared:${type.label}`)} + buttonValue="single" + isSelected={pipetteType === type.value} + /> + ) + })} + + + {pipetteType != null && robotType === OT2_ROBOT_TYPE ? ( + + + {t('pip_gen')} + + + {PIPETTE_GENS.map(gen => ( + { + setPipetteGen(gen) + setPipetteVolume(null) + setSelectedTips([]) + }} + buttonLabel={gen} + buttonValue={gen} + isSelected={pipetteGen === gen} + /> + ))} + + + ) : null} + {(pipetteType != null && robotType === FLEX_ROBOT_TYPE) || + (pipetteGen !== 'flex' && + pipetteType != null && + robotType === OT2_ROBOT_TYPE) ? ( + + + {t('pip_vol')} + + + {PIPETTE_VOLUMES[robotType]?.map(volume => { + if (robotType === FLEX_ROBOT_TYPE && pipetteType != null) { + const flexVolume = volume as PipetteInfoByType + const flexPipetteInfo = flexVolume[pipetteType] + + return flexPipetteInfo?.map(type => ( + { + setPipetteVolume(type.value) + setSelectedTips([]) + }} + buttonLabel={t('vol_label', { volume: type.label })} + buttonValue={type.value} + isSelected={pipetteVolume === type.value} + /> + )) + } else { + const ot2Volume = volume as PipetteInfoByGen + const gen = pipetteGen as Gen + + return ot2Volume[gen].map(info => { + return info[pipetteType]?.map(type => ( + { + setPipetteVolume(type.value) + }} + buttonLabel={t('vol_label', { + volume: type.label, + })} + buttonValue={type.value} + isSelected={pipetteVolume === type.value} + /> + )) + }) + } + })} + + + ) : null} + {allPipetteOptions.includes(selectedPip as PipetteName) + ? (() => { + const tiprackOptions = getTiprackOptions({ + allLabware: allLabware, + allowAllTipracks: allowAllTipracks, + selectedPipetteName: selectedPip, + }) + return ( + + + {t('pip_tips')} + + + {tiprackOptions.map(option => ( + { + const updatedTips = selectedTips.includes( + option.value + ) + ? selectedTips.filter(v => v !== option.value) + : [...selectedTips, option.value] + setSelectedTips(updatedTips) + }} + /> + ))} + + + + + {t('add_custom_tips')} + + dispatch(createCustomTiprackDef(e))} + /> + + {pipetteVolume === 'p1000' && + robotType === FLEX_ROBOT_TYPE ? null : ( + { + dispatch( + setFeatureFlags({ + OT_PD_ALLOW_ALL_TIPRACKS: !allowAllTipracks, + }) + ) + }} + textDecoration={TYPOGRAPHY.textDecorationUnderline} + > + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + + )} + + + ) + })() + : null} + + )} + , + getTopPortalEl() + ) +} + +const StyledLabel = styled.label` + text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; + font-size: ${PRODUCT.TYPOGRAPHY.fontSizeBodyDefaultSemiBold}; + display: ${DISPLAY_INLINE_BLOCK}; + cursor: pointer; + input[type='file'] { + display: none; + } +` diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/utils.ts b/protocol-designer/src/organisms/EditInstrumentsModal/utils.ts new file mode 100644 index 00000000000..81f810e2a70 --- /dev/null +++ b/protocol-designer/src/organisms/EditInstrumentsModal/utils.ts @@ -0,0 +1,30 @@ +import type { PipetteName, PipetteV2Specs } from '@opentrons/shared-data' +import type { + Gen, + PipetteType, +} from '../../pages/CreateNewProtocolWizard/types' + +export interface PipetteSections { + type: PipetteType + gen: Gen | 'flex' + volume: string +} + +export const getSectionsFromPipetteName = ( + pipetteName: PipetteName, + specs: PipetteV2Specs +): PipetteSections => { + const channels = specs.channels + let type: PipetteType = 'multi' + if (channels === 96) { + type = '96' + } else if (channels === 1) { + type = 'single' + } + const volume = pipetteName.split('_')[0] + return { + type, + gen: specs.displayCategory === 'FLEX' ? 'flex' : specs.displayCategory, + volume, + } +} diff --git a/protocol-designer/src/organisms/EditNickNameModal/__tests__/EditNickNameModal.test.tsx b/protocol-designer/src/organisms/EditNickNameModal/__tests__/EditNickNameModal.test.tsx new file mode 100644 index 00000000000..b398564c512 --- /dev/null +++ b/protocol-designer/src/organisms/EditNickNameModal/__tests__/EditNickNameModal.test.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { EditNickNameModal } from '..' +import { getLabwareNicknamesById } from '../../../ui/labware/selectors' +import { renameLabware } from '../../../labware-ingred/actions' + +vi.mock('../../../ui/labware/selectors') +vi.mock('../../../labware-ingred/actions') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('EditNickNameModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onClose: vi.fn(), + labwareId: 'mockId', + } + vi.mocked(getLabwareNicknamesById).mockReturnValue({ + mockId: 'mockOriginalName', + }) + }) + it('renders the text and adds a nickname', () => { + render(props) + screen.getByText('Rename labware') + screen.getByText('Labware name') + + fireEvent.click(screen.getByText('Cancel')) + expect(props.onClose).toHaveBeenCalled() + + const input = screen.getByRole('textbox') + fireEvent.change(input, { target: { value: 'mockNickName' } }) + fireEvent.click(screen.getByText('Save')) + expect(vi.mocked(renameLabware)).toHaveBeenCalled() + expect(props.onClose).toHaveBeenCalled() + }) + it('renders the too long nickname error', () => { + render(props) + const input = screen.getByRole('textbox') + fireEvent.change(input, { + target: { + value: + 'mockNickNameisthelongestnicknameihaveeverseen mockNickNameisthelongestnicknameihaveeverseen mockNickNameisthelongest', + }, + }) + screen.getByText('Labware names must be 115 characters or fewer.') + }) +}) diff --git a/protocol-designer/src/organisms/EditNickNameModal/index.tsx b/protocol-designer/src/organisms/EditNickNameModal/index.tsx new file mode 100644 index 00000000000..381db38c228 --- /dev/null +++ b/protocol-designer/src/organisms/EditNickNameModal/index.tsx @@ -0,0 +1,93 @@ +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + COLORS, + DIRECTION_COLUMN, + Flex, + InputField, + JUSTIFY_END, + Modal, + PrimaryButton, + SecondaryButton, + SPACING, + StyledText, +} from '@opentrons/components' +import { selectors as uiLabwareSelectors } from '../../ui/labware' +import { getTopPortalEl } from '../../components/portals/TopPortal' +import { renameLabware } from '../../labware-ingred/actions' +import type { ThunkDispatch } from '../../types' + +const MAX_NICK_NAME_LENGTH = 115 +interface EditNickNameModalProps { + labwareId: string + onClose: () => void +} +export function EditNickNameModal(props: EditNickNameModalProps): JSX.Element { + const { onClose, labwareId } = props + const { t } = useTranslation(['create_new_protocol', 'shared']) + const dispatch = useDispatch>() + const nickNames = useSelector(uiLabwareSelectors.getLabwareNicknamesById) + const savedNickname = nickNames[labwareId] + const [nickName, setNickName] = React.useState(savedNickname) + const saveNickname = (): void => { + dispatch(renameLabware({ labwareId, name: nickName })) + onClose() + } + + return createPortal( + + { + onClose() + }} + > + {t('shared:cancel')} + + = MAX_NICK_NAME_LENGTH} + > + {t('shared:save')} + + + } + > + + + + {t('labware_name')} + + + = MAX_NICK_NAME_LENGTH ? t('rename_error') : null + } + data-testid="renameLabware_inputField" + name="renameLabware" + onChange={e => { + setNickName(e.target.value) + }} + value={nickName} + type="text" + autoFocus + /> + + , + getTopPortalEl() + ) +} diff --git a/protocol-designer/src/organisms/EditProtocolMetadataModal/__tests__/EditProtocolMetadataModal.test.tsx b/protocol-designer/src/organisms/EditProtocolMetadataModal/__tests__/EditProtocolMetadataModal.test.tsx new file mode 100644 index 00000000000..60b02251bc3 --- /dev/null +++ b/protocol-designer/src/organisms/EditProtocolMetadataModal/__tests__/EditProtocolMetadataModal.test.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { EditProtocolMetadataModal } from '..' +import { selectors as fileSelectors } from '../../../file-data' + +vi.mock('../../../file-data') + +const render = ( + props: React.ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('EditProtocolMetadataModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onClose: vi.fn(), + } + vi.mocked(fileSelectors.getFileMetadata).mockReturnValue({ + protocolName: 'mockName', + author: 'mockAuthor', + description: 'mockDescription', + }) + }) + + it('renders all the text and fields', () => { + render(props) + screen.getByText('Edit protocol metadata') + screen.getByText('Name') + screen.getByText('Description') + screen.getByText('Author/Organization') + let input = screen.getAllByRole('textbox', { name: '' })[1] + fireEvent.change(input, { target: { value: 'mockProtocolName' } }) + input = screen.getAllByRole('textbox', { name: '' })[2] + fireEvent.change(input, { target: { value: 'mock org' } }) + screen.getByText('Save') + }) +}) diff --git a/protocol-designer/src/organisms/EditProtocolMetadataModal/index.tsx b/protocol-designer/src/organisms/EditProtocolMetadataModal/index.tsx new file mode 100644 index 00000000000..8bd57308d56 --- /dev/null +++ b/protocol-designer/src/organisms/EditProtocolMetadataModal/index.tsx @@ -0,0 +1,128 @@ +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import styled from 'styled-components' +import { useForm } from 'react-hook-form' +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + JUSTIFY_END, + Modal, + PrimaryButton, + SecondaryButton, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { getTopPortalEl } from '../../components/portals/TopPortal' +import { InputField } from '../../components/modals/CreateFileWizard/InputField' +import { actions, selectors as fileSelectors } from '../../file-data' +import type { FileMetadataFields } from '../../file-data' + +interface EditProtocolMetadataModalProps { + onClose: () => void +} +export function EditProtocolMetadataModal( + props: EditProtocolMetadataModalProps +): JSX.Element { + const { onClose } = props + const dispatch = useDispatch() + const { t } = useTranslation(['create_new_protocol', 'shared']) + const formValues = useSelector(fileSelectors.getFileMetadata) + const { + handleSubmit, + watch, + register, + formState: { isDirty }, + } = useForm({ defaultValues: formValues }) + const [protocolName, author, description] = watch([ + 'protocolName', + 'author', + 'description', + ]) + + const saveFileMetadata = (nextFormValues: FileMetadataFields): void => { + dispatch(actions.saveFileMetadata(nextFormValues)) + onClose() + } + + return createPortal( + + + {t('shared:cancel')} + + { + handleSubmit(saveFileMetadata)() + }} + > + {t('shared:save')} + + + } + > +
+ + + + {t('name')} + + + + + + {t('description')} + + + + + + + {t('author_org')} + + + + +
+
, + getTopPortalEl() + ) +} + +const DescriptionField = styled.textarea` + min-height: 5rem; + width: 100%; + border: ${BORDERS.lineBorder}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeH3}; + resize: none; +` diff --git a/protocol-designer/src/organisms/FileUploadMessagesModal/__tests__/FileUploadMessagesModal.test.tsx b/protocol-designer/src/organisms/FileUploadMessagesModal/__tests__/FileUploadMessagesModal.test.tsx new file mode 100644 index 00000000000..9915c380555 --- /dev/null +++ b/protocol-designer/src/organisms/FileUploadMessagesModal/__tests__/FileUploadMessagesModal.test.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { getFileUploadMessages } from '../../../load-file/selectors' +import { + dismissFileUploadMessage, + undoLoadFile, +} from '../../../load-file/actions' +import { useFileUploadModalContents } from '../utils' +import { FileUploadMessagesModal } from '..' + +vi.mock('../utils') +vi.mock('../../../load-file/selectors') +vi.mock('../../../load-file/actions') + +const render = () => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('FileUploadMessagesModal', () => { + beforeEach(() => { + vi.mocked(getFileUploadMessages).mockReturnValue({ + isError: true, + errorType: 'INVALID_FILE_TYPE', + }) + vi.mocked(useFileUploadModalContents).mockReturnValue({ + body: 'mockBody', + title: 'mockTitle', + }) + }) + + it('renders modal for invalid file', () => { + render() + screen.getByText('mockTitle') + screen.getByText('mockBody') + }) + it('renders modal for a migration', () => { + vi.mocked(getFileUploadMessages).mockReturnValue({ + isError: false, + messageKey: 'DID_MIGRATE', + migrationsRan: ['8.1.0'], + }) + render() + screen.getByText('mockTitle') + screen.getByText('mockBody') + fireEvent.click(screen.getByRole('button', { name: 'Confirm' })) + expect(vi.mocked(dismissFileUploadMessage)).toHaveBeenCalled() + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })) + expect(vi.mocked(undoLoadFile)).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/organisms/FileUploadMessagesModal/__tests__/utils.test.ts b/protocol-designer/src/organisms/FileUploadMessagesModal/__tests__/utils.test.ts new file mode 100644 index 00000000000..235ab6dc37e --- /dev/null +++ b/protocol-designer/src/organisms/FileUploadMessagesModal/__tests__/utils.test.ts @@ -0,0 +1,55 @@ +import { it, describe, expect } from 'vitest' +import { getMigrationMessage } from '../utils' + +const tMock = (key: string) => key + +describe('modalContents', () => { + describe('getMigrationMessage', () => { + it('should return the v3 migration message when migrating to v3', () => { + const migrationsList = [ + ['3.0.0'], + ['3.0.0', '4.0.0'], + ['3.0.0', '4.0.0', '5.0.0'], + ['3.0.0', '4.0.0', '5.0.0', '5.1.0'], + ['3.0.0', '4.0.0', '5.0.0', '5.1.0, 5.2.0'], + ] + migrationsList.forEach(migrations => { + expect( + JSON.stringify( + getMigrationMessage({ migrationsRan: migrations, t: tMock }) + ) + ).toEqual(expect.stringContaining('migrations.toV3Migration.title')) + }) + }) + it('should return the "no behavior change message" when migrating from v5.x to 6', () => { + const migrationsList = [ + ['5.0.0'], + ['5.0.0', '5.1.0'], + ['5.0.0', '5.1.0', '5.2.0'], + ] + migrationsList.forEach(migrations => { + expect( + JSON.stringify( + getMigrationMessage({ migrationsRan: migrations, t: tMock }) + ) + ).toEqual(expect.stringContaining('migrations.noBehaviorChange.body1')) + }) + }) + it('should return the generic migration modal when a v4 migration or v7 migration is required', () => { + const migrationsList = [ + ['4.0.0'], + ['4.0.0', '5.0.0'], + ['4.0.0', '5.0.0', '5.1.0'], + ['4.0.0', '5.0.0', '5.1.0, 5.2.0'], + ['6.0.0', '6.1.0', '6.2.0', '6.2.1', '6.2.2'], + ] + migrationsList.forEach(migrations => { + expect( + JSON.stringify( + getMigrationMessage({ migrationsRan: migrations, t: tMock }) + ) + ).toEqual(expect.stringContaining('migrations.generic.body1')) + }) + }) + }) +}) diff --git a/protocol-designer/src/organisms/FileUploadMessagesModal/index.tsx b/protocol-designer/src/organisms/FileUploadMessagesModal/index.tsx new file mode 100644 index 00000000000..419863570c1 --- /dev/null +++ b/protocol-designer/src/organisms/FileUploadMessagesModal/index.tsx @@ -0,0 +1,61 @@ +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { + Flex, + JUSTIFY_END, + Modal, + PrimaryButton, + SPACING, + SecondaryButton, +} from '@opentrons/components' +import { getFileUploadMessages } from '../../load-file/selectors' +import { dismissFileUploadMessage, undoLoadFile } from '../../load-file/actions' +import { useFileUploadModalContents } from './utils' + +export function FileUploadMessagesModal(): JSX.Element | null { + const message = useSelector(getFileUploadMessages) + const dispatch = useDispatch() + const { t } = useTranslation('shared') + const modalContents = useFileUploadModalContents({ + uploadResponse: message, + }) + const dismissModal = (): void => { + dispatch(dismissFileUploadMessage()) + } + + if (modalContents == null) return null + + const { title, body } = modalContents + const showButtons = + title !== t('invalid_json_file') && title !== t('incorrect_file_header') + + return ( + + { + dispatch(undoLoadFile()) + }} + > + {t('cancel')} + + {t('confirm')} + + ) + } + > + {body} + + ) +} diff --git a/protocol-designer/src/organisms/FileUploadMessagesModal/utils.tsx b/protocol-designer/src/organisms/FileUploadMessagesModal/utils.tsx new file mode 100644 index 00000000000..7e5069dff77 --- /dev/null +++ b/protocol-designer/src/organisms/FileUploadMessagesModal/utils.tsx @@ -0,0 +1,261 @@ +import * as React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { + COLORS, + DIRECTION_COLUMN, + Flex, + SPACING, + StyledText, +} from '@opentrons/components' + +import type { FileUploadMessage } from '../../load-file' + +export interface ModalContents { + title: string + body: React.ReactNode +} + +interface ModalProps { + t: any + errorMessage?: string | null +} + +const getInvalidFileType = (props: ModalProps): ModalContents => { + const { t } = props + return { + title: t('incorrect_file_header'), + body: ( + + {t('incorrect_file_type_body')} + + ), + } +} + +const invalidJsonModal = (props: ModalProps): ModalContents => { + const { t, errorMessage } = props + return { + title: t('invalid_json_file'), + body: ( + + + {t('invalid_json_file_body')} + + + + {t('invalid_json_file_error')} + + + {errorMessage} + + + + ), + } +} + +export const getGenericDidMigrateMessage = ( + props: ModalProps +): ModalContents => { + const { t } = props + + return { + title: t('migration_header'), + body: ( + + + {t('migrations.generic.body1')} + + + {t('migrations.generic.body2')} + + + {t('migrations.generic.body3')} + + + ), + } +} + +export const getNoBehaviorChangeMessage = ( + props: ModalProps +): ModalContents => { + const { t } = props + + return { + title: t('migration_header'), + body: ( + + + {t('migrations.noBehaviorChange.body1')} + + + {t('migrations.noBehaviorChange.body2')} + + + {t('migrations.noBehaviorChange.body3')} + + + ), + } +} + +export const getToV8MigrationMessage = (props: ModalProps): ModalContents => { + const { t } = props + + return { + title: t('migration_header'), + body: ( + + + {t('migrations.toV8Migration.body1')} + + + {t('migrations.toV8Migration.body2')} + + + {t('migrations.toV8Migration.body3')} + + + {t('migrations.toV8Migration.body4')} + + + ), + } +} + +export const getToV8_1MigrationMessage = (props: ModalProps): ModalContents => { + const { t } = props + + return { + title: t('migration_header'), + + body: ( + + + {t('migrations.toV8_1Migration.body1')} + + + {t('migrations.toV8_1Migration.body2')} + + + {t('migrations.toV8_1Migration.body3')} + + + ), + } +} + +export const getToV3MigrationMessage = (props: ModalProps): ModalContents => { + const { t } = props + + return { + title: t('migrations.toV3Migration.title'), + body: ( + + + }} + /> + + + {t('migrations.toV3Migration.body2')} + + + {t('migrations.toV3Migration.body3')} + + + {t('migrations.toV3Migration.body4')} + + + {t('migrations.toV3Migration.body5')} + + + {t('migrations.toV3Migration.body6')} + + + ), + } +} + +interface MigrationMessageProps { + migrationsRan: string[] + t: any +} + +export const getMigrationMessage = ( + props: MigrationMessageProps +): ModalContents => { + const { t, migrationsRan } = props + + if (migrationsRan.includes('3.0.0')) { + return getToV3MigrationMessage({ t }) + } + const noBehaviorMigrations = [ + ['5.0.0'], + ['5.0.0', '5.1.0'], + ['5.0.0', '5.1.0', '5.2.0'], + ] + if ( + noBehaviorMigrations.some(migrationList => + migrationsRan.every(migration => migrationList.includes(migration)) + ) + ) { + return getNoBehaviorChangeMessage({ t }) + } + if (migrationsRan.includes('8.1.0')) { + return getToV8_1MigrationMessage({ t }) + } else if (migrationsRan.includes('8.0.0')) { + return getToV8MigrationMessage({ t }) + } + + return getGenericDidMigrateMessage({ t }) +} + +interface FileUploadModalContentsProps { + uploadResponse?: FileUploadMessage | null +} +export function useFileUploadModalContents( + props: FileUploadModalContentsProps +): ModalContents | null { + const { uploadResponse } = props + const { t } = useTranslation('shared') + + if (uploadResponse == null) return null + + if (uploadResponse.isError) { + switch (uploadResponse.errorType) { + case 'INVALID_FILE_TYPE': + return getInvalidFileType({ t }) + case 'INVALID_JSON_FILE': + return invalidJsonModal({ + errorMessage: uploadResponse.errorMessage, + t, + }) + default: { + console.error('Invalid error type specified for modal') + return null + } + } + } + switch (uploadResponse.messageKey) { + case 'DID_MIGRATE': + return getMigrationMessage({ + migrationsRan: uploadResponse.migrationsRan, + t, + }) + default: { + console.assert( + false, + `invalid messageKey ${uploadResponse.messageKey} specified for modal` + ) + return { title: '', body: uploadResponse.messageKey } + } + } +} diff --git a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx new file mode 100644 index 00000000000..502bb1b784f --- /dev/null +++ b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { setFeatureFlags } from '../../../feature-flags/actions' +import { IncompatibleTipsModal } from '..' + +vi.mock('../../../feature-flags/actions') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('IncompatibleTipsModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onClose: vi.fn(), + } + }) + it('renders the text and ctas', () => { + render(props) + screen.getByText('Incompatible tips') + screen.getByText( + 'Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.' + ) + fireEvent.click(screen.getByText('Show incompatible tips')) + expect(vi.mocked(setFeatureFlags)).toHaveBeenCalled() + fireEvent.click(screen.getByText('Cancel')) + expect(props.onClose).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/organisms/IncompatibleTipsModal/index.tsx b/protocol-designer/src/organisms/IncompatibleTipsModal/index.tsx new file mode 100644 index 00000000000..1d92a52377a --- /dev/null +++ b/protocol-designer/src/organisms/IncompatibleTipsModal/index.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { useDispatch } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { + Flex, + JUSTIFY_END, + Modal, + PrimaryButton, + SecondaryButton, + SPACING, + StyledText, +} from '@opentrons/components' +import { setFeatureFlags } from '../../feature-flags/actions' +import type { ThunkDispatch } from 'redux-thunk' +import type { BaseState } from '../../types' + +interface IncompatibleTipsProps { + onClose: () => void +} +export function IncompatibleTipsModal( + props: IncompatibleTipsProps +): JSX.Element { + const { onClose } = props + const dispatch = useDispatch>() + const { t } = useTranslation(['create_new_protocol', 'shared']) + + return ( + + { + onClose() + dispatch( + setFeatureFlags({ + OT_PD_ALLOW_ALL_TIPRACKS: true, + }) + ) + }} + > + {t('show_tips')} + + {t('shared:cancel')} + + } + > + + {t('incompatible_tip_body')} + + + ) +} diff --git a/protocol-designer/src/organisms/LabwareUploadModal/LabwareUploadModalBody.tsx b/protocol-designer/src/organisms/LabwareUploadModal/LabwareUploadModalBody.tsx new file mode 100644 index 00000000000..a968ca90de2 --- /dev/null +++ b/protocol-designer/src/organisms/LabwareUploadModal/LabwareUploadModalBody.tsx @@ -0,0 +1,95 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + DIRECTION_COLUMN, + Flex, + SPACING, + StyledText, +} from '@opentrons/components' +import type { LabwareUploadMessage } from '../../labware-defs' + +export function LabwareUploadModalBody(props: { + message: LabwareUploadMessage +}): JSX.Element | null { + const { message } = props + const { t } = useTranslation('shared') + + const validMessageTypes = [ + 'EXACT_LABWARE_MATCH', + 'INVALID_JSON_FILE', + 'ONLY_TIPRACK', + 'NOT_JSON', + 'USES_STANDARD_NAMESPACE', + ] + + if (validMessageTypes.includes(message.messageType)) { + return ( + + + {t(`message_${message.messageType.toLowerCase()}`)} + + {'errorText' in message ? ( + + {message.errorText} + + ) : null} + + ) + } else if ( + message.messageType === 'ASK_FOR_LABWARE_OVERWRITE' || + message.messageType === 'LABWARE_NAME_CONFLICT' + ) { + const { defsMatchingDisplayName, defsMatchingLoadName } = message + const canOverwrite = message.messageType === 'ASK_FOR_LABWARE_OVERWRITE' + return ( + + + {t('shares_name', { + customOrStandard: canOverwrite ? 'custom' : 'Opentrons standard', + })} + + {canOverwrite && defsMatchingLoadName.length > 0 ? ( + + + {t('shared_load_name')} + {defsMatchingLoadName + .map(def => def?.parameters.loadName || '?') + .join(', ')} + + + {t('shared_display_name')} + {defsMatchingDisplayName + .map(def => def?.metadata.displayName || '?') + .join(', ')} + + + ) : null} + + {t('re_export')} + + {canOverwrite ? ( + + {t('overwrite')} + + ) : null} + {canOverwrite && + 'isOverwriteMismatched' in message && + message.isOverwriteMismatched ? ( + + + {t('labware_upload_message.name_conflict.warning')} + + + {t('labware_upload_message.name_conflict.mismatched')} + + + ) : null} + + ) + } + console.assert( + false, + `MessageBody got unhandled messageType: ${message.messageType}` + ) + return null +} diff --git a/protocol-designer/src/organisms/LabwareUploadModal/_tests__/LabwareUploadModal.test.tsx b/protocol-designer/src/organisms/LabwareUploadModal/_tests__/LabwareUploadModal.test.tsx new file mode 100644 index 00000000000..94da455a34e --- /dev/null +++ b/protocol-designer/src/organisms/LabwareUploadModal/_tests__/LabwareUploadModal.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { LabwareUploadModal } from '..' +import { getLabwareUploadMessage } from '../../../labware-defs/selectors' +import { dismissLabwareUploadMessage } from '../../../labware-defs/actions' + +vi.mock('../../../labware-defs/selectors') +vi.mock('../../../labware-defs/actions') + +const render = () => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('LabwareUploadModal', () => { + beforeEach(() => { + vi.mocked(getLabwareUploadMessage).mockReturnValue({ + messageType: 'NOT_JSON', + }) + }) + + it('renders modal for not json', () => { + render() + screen.getByText('Protocol Designer only accepts JSON files.') + screen.getByText('Incompatible file type') + fireEvent.click( + screen.getByTestId('ModalHeader_icon_close_Incompatible file type') + ) + expect(vi.mocked(dismissLabwareUploadMessage)).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/organisms/LabwareUploadModal/index.tsx b/protocol-designer/src/organisms/LabwareUploadModal/index.tsx new file mode 100644 index 00000000000..31cd291f4c7 --- /dev/null +++ b/protocol-designer/src/organisms/LabwareUploadModal/index.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { + Flex, + JUSTIFY_END, + Modal, + PrimaryButton, + SPACING, + SecondaryButton, +} from '@opentrons/components' +import { getLabwareUploadMessage } from '../../labware-defs/selectors' +import { + dismissLabwareUploadMessage, + replaceCustomLabwareDef, +} from '../../labware-defs/actions' +import { LabwareUploadModalBody } from './LabwareUploadModalBody' + +export function LabwareUploadModal(): JSX.Element | null { + const message = useSelector(getLabwareUploadMessage) + const dispatch = useDispatch() + const { t } = useTranslation('shared') + const dismissModal = (): void => { + dispatch(dismissLabwareUploadMessage()) + } + const overwriteLabwareDef = (): void => { + if (message && message.messageType === 'ASK_FOR_LABWARE_OVERWRITE') { + dispatch( + replaceCustomLabwareDef({ + defURIToOverwrite: message.defURIToOverwrite, + newDef: message.newDef, + isOverwriteMismatched: message.isOverwriteMismatched, + }) + ) + } else { + console.assert( + false, + `labware def should only be overwritten when messageType is ASK_FOR_LABWARE_OVERWRITE. Got ${message?.messageType}` + ) + } + } + + if (message == null) return null + + return ( + + + {t('cancel')} + + + {t('overwrite_labware')} + + + ) + } + > + + + ) +} diff --git a/protocol-designer/src/organisms/MaterialsListModal/MaterialsListModal.stories.tsx b/protocol-designer/src/organisms/MaterialsListModal/MaterialsListModal.stories.tsx new file mode 100644 index 00000000000..2a75c7e76da --- /dev/null +++ b/protocol-designer/src/organisms/MaterialsListModal/MaterialsListModal.stories.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import { I18nextProvider } from 'react-i18next' +import { i18n } from '../../assets/localization' +import { MaterialsListModal as MaterialsListModalComponent } from '.' + +import type { Meta, StoryObj } from '@storybook/react' +import type { LabwareOnDeck, ModuleOnDeck } from '../../step-forms' +import type { FixtureInList } from '.' + +const mockHardware = [ + { + id: 'mockHardware', + model: 'temperatureModuleV2', + moduleState: { + type: 'temperatureModuleType', + status: 'TEMPERATURE_DEACTIVATED', + targetTemperature: null, + }, + slot: 'C1', + type: 'temperatureModuleType', + }, +] as ModuleOnDeck[] + +const mockFixture = [ + { location: 'cutoutB3', name: 'trashBin', id: 'mockId:trashBin' }, +] as FixtureInList[] + +const mockLabware = [ + { + def: { + metadata: { + displayCategory: 'tipRack', + displayName: 'Opentrons Flex 96 Filter Tip Rack 50 µL', + displayVolumeUnits: 'µL', + tags: [], + namespace: 'opentrons', + } as any, + }, + id: 'mockLabware', + labwareDefURI: 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + slot: 'D3', + }, +] as LabwareOnDeck[] + +// ToDo (kk:09/03/2024) add test when implementing liquids part completely +const mockLiquids = [] as any[] + +const meta: Meta = { + title: 'Protocol-Designer/Organisms/MaterialsListModal', + component: MaterialsListModalComponent, + decorators: [ + Story => ( + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +export const MaterialsListModal: Story = { + args: { + hardware: mockHardware, + fixtures: mockFixture, + labware: mockLabware, + liquids: mockLiquids, + }, +} + +export const EmptyMaterialsListModal: Story = { + args: { + hardware: [], + fixtures: [], + labware: [], + liquids: [], + }, +} diff --git a/protocol-designer/src/organisms/MaterialsListModal/__tests__/MaterialsListModal.test.tsx b/protocol-designer/src/organisms/MaterialsListModal/__tests__/MaterialsListModal.test.tsx new file mode 100644 index 00000000000..9d70b0eb0e3 --- /dev/null +++ b/protocol-designer/src/organisms/MaterialsListModal/__tests__/MaterialsListModal.test.tsx @@ -0,0 +1,175 @@ +import * as React from 'react' +import { describe, it, beforeEach, vi, expect } from 'vitest' +import { screen } from '@testing-library/react' + +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' + +import { i18n } from '../../../assets/localization' +import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors' +import { renderWithProviders } from '../../../__testing-utils__' +import { getRobotType } from '../../../file-data/selectors' +import { getInitialDeckSetup } from '../../../step-forms/selectors' +import { MaterialsListModal } from '..' + +import type { InfoScreen } from '@opentrons/components' +import type { LabwareOnDeck, ModuleOnDeck } from '../../../step-forms' +import type { FixtureInList } from '..' + +vi.mock('../../../step-forms/selectors') +vi.mock('../../../labware-ingred/selectors') +vi.mock('../../../file-data/selectors') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + InfoScreen: () =>
mock InfoScreen
, + } +}) + +const mockSetShowMaterialsListModal = vi.fn() + +const mockHardWare = [ + { + id: 'mockHardware', + model: 'temperatureModuleV2', + moduleState: { + type: 'temperatureModuleType', + status: 'TEMPERATURE_DEACTIVATED', + targetTemperature: null, + }, + slot: 'C1', + type: 'temperatureModuleType', + }, +] as ModuleOnDeck[] + +const mockFixture = [ + { location: 'cutoutB3', name: 'trashBin', id: 'mockId:trashBin' }, +] as FixtureInList[] + +const mockLabware = [ + { + def: { + metadata: { + displayCategory: 'tipRack', + displayName: 'Opentrons Flex 96 Filter Tip Rack 50 µL', + displayVolumeUnits: 'µL', + tags: [], + namespace: 'opentrons', + } as any, + }, + id: 'mockLabware', + labwareDefURI: 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + slot: 'D3', + }, +] as LabwareOnDeck[] + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('MaterialsListModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + hardware: [], + fixtures: [], + labware: [], + liquids: [], + setShowMaterialsListModal: mockSetShowMaterialsListModal, + } + vi.mocked(getInitialDeckSetup).mockReturnValue({ + labware: {}, + modules: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + }) + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({}) + }) + + it('should render render text', () => { + render(props) + screen.getByText('Materials list') + screen.getByText('Deck hardware') + screen.getByText('Labware') + screen.getByText('Liquids') + }) + + it('should render InfoScreen component', () => { + render(props) + expect(screen.getAllByText('mock InfoScreen').length).toBe(3) + }) + + it('should render hardware info', () => { + props = { + ...props, + hardware: mockHardWare, + fixtures: mockFixture, + } + render(props) + screen.getByText('C1') + screen.getByText('Temperature Module GEN2') + screen.getByText('B3') + screen.getByText('Trash Bin') + }) + it('should render labware info', () => { + props = { + ...props, + labware: mockLabware, + } + render(props) + screen.getByText('D3') + screen.getByText('Opentrons Flex 96 Filter Tip Rack 50 µL') + }) + + it('should render 7,8,10,11 when a robot is ot-2 and a module is tc', () => { + vi.mocked(getRobotType).mockReturnValue(OT2_ROBOT_TYPE) + const mockHardwareForOt2 = [ + { + id: 'mockHardware-tc', + model: 'thermocyclerModuleV1', + moduleState: { + type: 'thermocyclerModuleType', + blockTargetTemp: null, + lidTargetTemp: null, + lidOpen: false, + }, + slot: 'span7_8_10_11', + type: 'thermocyclerModuleType', + }, + ] as ModuleOnDeck[] + props = { + ...props, + hardware: mockHardwareForOt2, + } + render(props) + screen.getByText('7,8,10,11') + }) + + it('should render liquids info', () => { + const mockId = 'mockId' + vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({ + labware1: { well1: { [mockId]: { volume: 10 } } }, + }) + props = { + ...props, + + liquids: [ + { + ingredientId: mockId, + name: 'mockName', + displayColor: 'mockDisplayColor', + }, + ], + } + render(props) + screen.getByText('Liquids') + screen.getByText('Name') + screen.getByText('Total Well Volume') + screen.getByText('mockName') + screen.getByText('10 uL') + }) +}) diff --git a/protocol-designer/src/organisms/MaterialsListModal/index.tsx b/protocol-designer/src/organisms/MaterialsListModal/index.tsx new file mode 100644 index 00000000000..766aac528ef --- /dev/null +++ b/protocol-designer/src/organisms/MaterialsListModal/index.tsx @@ -0,0 +1,281 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { useSelector } from 'react-redux' +import sum from 'lodash/sum' + +import { + ALIGN_CENTER, + COLORS, + DeckInfoLabel, + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + InfoScreen, + JUSTIFY_SPACE_BETWEEN, + LiquidIcon, + ListItem, + ListItemDescriptor, + Modal, + ModuleIcon, + SPACING, + StyledText, + Tag, +} from '@opentrons/components' +import { + FLEX_ROBOT_TYPE, + getModuleDisplayName, + THERMOCYCLER_MODULE_TYPE, +} from '@opentrons/shared-data' + +import { getRobotType } from '../../file-data/selectors' +import { getInitialDeckSetup } from '../../step-forms/selectors' +import { getTopPortalEl } from '../../components/portals/TopPortal' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' + +import type { AdditionalEquipmentName } from '@opentrons/step-generation' +import type { LabwareOnDeck, ModuleOnDeck } from '../../step-forms' +import type { OrderedLiquids } from '../../labware-ingred/types' + +// ToDo (kk:09/04/2024) this should be removed when break-point is set up +const MODAL_MIN_WIDTH = '36.1875rem' + +export interface FixtureInList { + name: AdditionalEquipmentName + id: string + location?: string +} + +interface MaterialsListModalProps { + hardware: ModuleOnDeck[] + fixtures: FixtureInList[] + labware: LabwareOnDeck[] + liquids: OrderedLiquids + setShowMaterialsListModal: (showMaterialsListModal: boolean) => void +} + +export function MaterialsListModal({ + hardware, + fixtures, + labware, + liquids, + setShowMaterialsListModal, +}: MaterialsListModalProps): JSX.Element { + const { t } = useTranslation(['protocol_overview', 'shared']) + const robotType = useSelector(getRobotType) + const deckSetup = useSelector(getInitialDeckSetup) + const { modules: modulesOnDeck, labware: labwareOnDeck } = deckSetup + const allLabwareWellContents = useSelector( + labwareIngredSelectors.getLiquidsByLabwareId + ) + const tCSlot = robotType === FLEX_ROBOT_TYPE ? 'A1, B1' : '7,8,10,11' + + return createPortal( + { + setShowMaterialsListModal(false) + }} + closeOnOutsideClick + title={t('materials_list')} + marginLeft="0rem" + minWidth={MODAL_MIN_WIDTH} + > + + + + {t('deck_hardware')} + + + {fixtures.length > 0 + ? fixtures.map(fixture => ( + + + ) : ( + '' + ) + } + content={ + + + {t(`shared:${fixture.name}`)} + + + } + /> + + )) + : null} + {hardware.length > 0 ? ( + hardware.map((hw, id) => { + const formatLocation = (slot: string): string => { + if (hw.type === THERMOCYCLER_MODULE_TYPE) { + return tCSlot + } + return slot.replace('cutout', '') + } + return ( + + + } + content={ + + + + {getModuleDisplayName(hw.model)} + + + } + /> + + ) + }) + ) : ( + + )} + + + + + + {t('labware')} + + + {labware.length > 0 ? ( + labware.map(lw => { + const labwareOnModuleEntity = Object.values(modulesOnDeck).find( + mod => mod.id === lw.slot + ) + const labwareOnLabwareEntity = Object.values( + labwareOnDeck + ).find(labware => labware.id === lw.slot) + const labwareOnLabwareOnModuleSlot = Object.values( + modulesOnDeck + ).find(mod => mod.id === labwareOnLabwareEntity?.slot)?.slot + const labwareOnLabwareOnSlot = labwareOnLabwareEntity?.slot + + let deckLabelSlot = lw.slot + if (labwareOnModuleEntity != null) { + deckLabelSlot = + labwareOnModuleEntity.type === THERMOCYCLER_MODULE_TYPE + ? tCSlot + : labwareOnModuleEntity.slot + } else if (labwareOnLabwareOnModuleSlot != null) { + deckLabelSlot = labwareOnLabwareOnModuleSlot + } else if (labwareOnLabwareOnSlot != null) { + deckLabelSlot = labwareOnLabwareOnSlot + } else if (deckLabelSlot === 'offDeck') { + deckLabelSlot = 'Off-deck' + } + return ( + + } + content={lw.def.metadata.displayName} + /> + + ) + }) + ) : ( + + )} + + + + + + {t('liquids')} + + + {liquids.length > 0 ? ( + + + {t('name')} + + + {t('total_well_volume')} + + + ) : null} + + {liquids.length > 0 ? ( + liquids.map((liquid, id) => { + const volumePerWell = Object.values( + allLabwareWellContents + ).flatMap(labwareWithIngred => + Object.values(labwareWithIngred).map( + ingred => ingred[liquid.ingredientId]?.volume ?? 0 + ) + ) + const totalVolume = sum(volumePerWell) + + if (totalVolume === 0) { + return null + } else { + return ( + + + + + + {liquid.name ?? t('n/a')} + + + + + + + + + ) + } + }) + ) : ( + + )} + + + + + , + getTopPortalEl() + ) +} diff --git a/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx b/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx new file mode 100644 index 00000000000..37a6d0eeba5 --- /dev/null +++ b/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { getLabwareDefsByURI } from '../../../labware-defs/selectors' +import { PipetteInfoItem } from '..' + +vi.mock('../../../labware-defs/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('PipetteInfoItem', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + editClick: vi.fn(), + cleanForm: vi.fn(), + tiprackDefURIs: ['mockDefUri'], + pipetteName: 'p1000_single', + mount: 'left', + formPipettesByMount: { + left: { pipetteName: 'p1000_single' }, + right: { pipetteName: 'p50_single' }, + }, + } + + vi.mocked(getLabwareDefsByURI).mockReturnValue({ + mockDefUri: { metadata: { displayName: 'mock display name' } } as any, + }) + }) + it('renders pipette with edit and remove buttons', () => { + render(props) + screen.getByText('P1000 Single-Channel GEN1') + screen.getByText('Left pipette') + screen.getByText('mock display name') + fireEvent.click(screen.getByText('Edit')) + expect(props.editClick).toHaveBeenCalled() + fireEvent.click(screen.getByText('Remove')) + expect(props.cleanForm).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/PipetteInfoItem.tsx b/protocol-designer/src/organisms/PipetteInfoItem/index.tsx similarity index 85% rename from protocol-designer/src/pages/CreateNewProtocolWizard/PipetteInfoItem.tsx rename to protocol-designer/src/organisms/PipetteInfoItem/index.tsx index 311c3a932ee..4628fc7481b 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/PipetteInfoItem.tsx +++ b/protocol-designer/src/organisms/PipetteInfoItem/index.tsx @@ -13,19 +13,19 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { getPipetteSpecsV2 } from '@opentrons/shared-data' +import { BUTTON_LINK_STYLE } from '../../atoms' import { getLabwareDefsByURI } from '../../labware-defs/selectors' -import type { UseFormSetValue, UseFormWatch } from 'react-hook-form' import type { PipetteMount, PipetteName } from '@opentrons/shared-data' -import type { WizardFormState } from './types' +import type { FormPipettesByMount, PipetteOnDeck } from '../../step-forms' interface PipetteInfoItemProps { mount: PipetteMount pipetteName: PipetteName tiprackDefURIs: string[] editClick: () => void - setValue: UseFormSetValue cleanForm: () => void - watch: UseFormWatch + formPipettesByMount?: FormPipettesByMount + pipetteOnDeck?: PipetteOnDeck[] } export function PipetteInfoItem(props: PipetteInfoItemProps): JSX.Element { @@ -34,11 +34,10 @@ export function PipetteInfoItem(props: PipetteInfoItemProps): JSX.Element { pipetteName, tiprackDefURIs, editClick, - setValue, cleanForm, - watch, + formPipettesByMount, + pipetteOnDeck, } = props - const pipettesByMount = watch('pipettesByMount') const { t, i18n } = useTranslation('create_new_protocol') const oppositeMount = mount === 'left' ? 'right' : 'left' const allLabware = useSelector(getLabwareDefsByURI) @@ -81,19 +80,21 @@ export function PipetteInfoItem(props: PipetteInfoItemProps): JSX.Element { {t('edit')} - {pipettesByMount[oppositeMount].pipetteName == null ? null : ( + {(formPipettesByMount != null && + formPipettesByMount[oppositeMount].pipetteName == null) || + (pipetteOnDeck != null && pipetteOnDeck.length === 1) ? null : ( { - setValue(`pipettesByMount.${mount}.pipetteName`, undefined) - setValue(`pipettesByMount.${mount}.tiprackDefURI`, undefined) cleanForm() }} textDecoration={TYPOGRAPHY.textDecorationUnderline} + css={BUTTON_LINK_STYLE} > {t('remove')} diff --git a/protocol-designer/src/organisms/ProtocolMetadataNav/index.tsx b/protocol-designer/src/organisms/ProtocolMetadataNav/index.tsx new file mode 100644 index 00000000000..fdbfe7188b4 --- /dev/null +++ b/protocol-designer/src/organisms/ProtocolMetadataNav/index.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' +import { useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { + COLORS, + DIRECTION_COLUMN, + Flex, + JUSTIFY_CENTER, + StyledText, +} from '@opentrons/components' +import { getFileMetadata } from '../../file-data/selectors' + +export function ProtocolMetadataNav(): JSX.Element { + const metadata = useSelector(getFileMetadata) + const { t } = useTranslation('starting_deck_state') + + return ( + + + {metadata?.protocolName != null && metadata?.protocolName !== '' + ? metadata?.protocolName + : t('untitled_protocol')} + + + + {t('edit_protocol')} + + + + ) +} diff --git a/protocol-designer/src/organisms/SlotDetailsContainer/index.tsx b/protocol-designer/src/organisms/SlotDetailsContainer/index.tsx new file mode 100644 index 00000000000..06c1add7885 --- /dev/null +++ b/protocol-designer/src/organisms/SlotDetailsContainer/index.tsx @@ -0,0 +1,134 @@ +import * as React from 'react' +import { useLocation } from 'react-router-dom' +import { useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { getModuleDisplayName } from '@opentrons/shared-data' +import { RobotCoordsForeignObject } from '@opentrons/components' +import * as wellContentsSelectors from '../../top-selectors/well-contents' +import { selectors } from '../../labware-ingred/selectors' +import { selectors as uiLabwareSelectors } from '../../ui/labware' +import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' +import { SlotInformation } from '../../organisms/SlotInformation' +import { getYPosition } from './utils' + +import type { DeckSlotId, RobotType } from '@opentrons/shared-data' +import type { ContentsByWell } from '../../labware-ingred/types' + +interface SlotDetailContainerProps { + robotType: RobotType + slot: DeckSlotId | null + offDeckLabwareId?: string | null +} + +export function SlotDetailsContainer( + props: SlotDetailContainerProps +): JSX.Element | null { + const { robotType, slot, offDeckLabwareId } = props + const { t } = useTranslation('shared') + const location = useLocation() + const deckSetup = useSelector(getDeckSetupForActiveItem) + const allWellContentsForActiveItem = useSelector( + wellContentsSelectors.getAllWellContentsForActiveItem + ) + const nickNames = useSelector(uiLabwareSelectors.getLabwareNicknamesById) + const allIngredNamesIds = useSelector(selectors.allIngredientNamesIds) + + if (slot == null || (slot === 'offDeck' && offDeckLabwareId == null)) { + return null + } + + const { + modules: deckSetupModules, + labware: deckSetupLabwares, + additionalEquipmentOnDeck, + } = deckSetup + + const offDeckLabwareNickName = + offDeckLabwareId != null ? nickNames[offDeckLabwareId] : null + + const moduleOnSlot = Object.values(deckSetupModules).find( + module => module.slot === slot + ) + const labwareOnSlot = Object.values(deckSetupLabwares).find( + lw => lw.slot === slot || lw.slot === moduleOnSlot?.id + ) + const nestedLabwareOnSlot = Object.values(deckSetupLabwares).find( + lw => lw.slot === labwareOnSlot?.id + ) + const fixturesOnSlot = Object.values(additionalEquipmentOnDeck).filter( + ae => ae.location?.split('cutout')[1] === slot + ) + const fixtureDisplayNames: string[] = fixturesOnSlot.map(fixture => + t(`${fixture.name}`) + ) + const moduleDisplayName = + moduleOnSlot != null ? getModuleDisplayName(moduleOnSlot.model) : null + + const liquidsLabware = + nestedLabwareOnSlot != null ? nestedLabwareOnSlot : labwareOnSlot + + let wellContents: ContentsByWell | null = null + if (offDeckLabwareId != null && allWellContentsForActiveItem != null) { + wellContents = allWellContentsForActiveItem[offDeckLabwareId] + } else if (allWellContentsForActiveItem != null && liquidsLabware != null) { + wellContents = allWellContentsForActiveItem[liquidsLabware.id] + } + + const liquids = + wellContents != null + ? Object.values(wellContents).flatMap(content => content.groupIds) + : null + + const uniqueLiquids = Array.from(new Set(liquids)) + + const liquidNamesOnLabware = uniqueLiquids + .map(liquid => { + const foundLiquid = Object.values(allIngredNamesIds).find( + id => id.ingredientId === liquid + ) + return foundLiquid?.name ?? '' + }) + .filter(Boolean) + + const labwares: string[] = [] + const adapters: string[] = [] + if (offDeckLabwareNickName != null) { + labwares.push(offDeckLabwareNickName) + } else { + if (nestedLabwareOnSlot != null && labwareOnSlot != null) { + adapters.push(nickNames[labwareOnSlot.id]) + labwares.push(nickNames[nestedLabwareOnSlot.id]) + } else if (nestedLabwareOnSlot == null && labwareOnSlot != null) { + labwares.push(nickNames[labwareOnSlot.id]) + } + } + + return location.pathname === '/designer' && slot !== 'offDeck' ? ( + + + + ) : ( + + ) +} diff --git a/protocol-designer/src/organisms/SlotDetailsContainer/utils.ts b/protocol-designer/src/organisms/SlotDetailsContainer/utils.ts new file mode 100644 index 00000000000..e103d6fab3e --- /dev/null +++ b/protocol-designer/src/organisms/SlotDetailsContainer/utils.ts @@ -0,0 +1,54 @@ +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import type { DeckSlotId, RobotType } from '@opentrons/shared-data' + +const FLEX_TOP_ROW_SLOTS = ['A1', 'A2', 'A3', 'A4'] +const FLEX_TOP_MIDDLE_ROW_SLOTS = ['B1', 'B2', 'B3', 'B4'] +const FLEX_BOTTOM_MIDDLE_ROW_SLOTS = ['C1', 'C2', 'C3', 'C4'] + +const OT2_TOP_ROW_SLOTS = ['10', '11'] +const OT2_TOP_MIDDLE_ROW_SLOTS = ['7', '8', '9'] +const OT2_BOTTOM_MIDDLE_ROW_SLOTS = ['4', '5', '6'] + +interface YPositionProps { + robotType: RobotType + slot: DeckSlotId +} + +const Y_POSITIONS = { + FLEX: { + TOP: '-10', + TOP_MIDDLE: '-110', + BOTTOM_MIDDLE: '-230', + BOTTOM: '-330', + }, + OT2: { + TOP: '-60', + TOP_MIDDLE: '-160', + BOTTOM_MIDDLE: '-250', + BOTTOM: '-340', + }, +} + +export const getYPosition = ({ robotType, slot }: YPositionProps): string => { + if (robotType === FLEX_ROBOT_TYPE) { + if (FLEX_TOP_ROW_SLOTS.includes(slot)) { + return Y_POSITIONS.FLEX.TOP + } else if (FLEX_TOP_MIDDLE_ROW_SLOTS.includes(slot)) { + return Y_POSITIONS.FLEX.TOP_MIDDLE + } else if (FLEX_BOTTOM_MIDDLE_ROW_SLOTS.includes(slot)) { + return Y_POSITIONS.FLEX.BOTTOM_MIDDLE + } else { + return Y_POSITIONS.FLEX.BOTTOM + } + } else { + if (OT2_TOP_ROW_SLOTS.includes(slot)) { + return Y_POSITIONS.OT2.TOP + } else if (OT2_TOP_MIDDLE_ROW_SLOTS.includes(slot)) { + return Y_POSITIONS.OT2.TOP_MIDDLE + } else if (OT2_BOTTOM_MIDDLE_ROW_SLOTS.includes(slot)) { + return Y_POSITIONS.OT2.BOTTOM_MIDDLE + } else { + return Y_POSITIONS.OT2.BOTTOM + } + } +} diff --git a/protocol-designer/src/organisms/SlotInformation/__tests__/SlotInformation.test.tsx b/protocol-designer/src/organisms/SlotInformation/__tests__/SlotInformation.test.tsx index 4a9c7bb91d4..247b96caa74 100644 --- a/protocol-designer/src/organisms/SlotInformation/__tests__/SlotInformation.test.tsx +++ b/protocol-designer/src/organisms/SlotInformation/__tests__/SlotInformation.test.tsx @@ -1,16 +1,29 @@ import * as React from 'react' -import { describe, it, beforeEach, expect } from 'vitest' +import { describe, it, beforeEach, expect, vi } from 'vitest' import { screen } from '@testing-library/react' - +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../assets/localization' import { SlotInformation } from '..' +import type { NavigateFunction } from 'react-router-dom' + const mockLiquids = ['Mastermix', 'Ethanol', 'Water'] -const mockLabwares = ['96 Well Plate', 'Adapter'] +const mockLabwares = ['96 Well Plate'] +const mockAdapters = ['Adapter'] const mockModules = ['Thermocycler Module Gen2', 'Heater-Shaker Module'] +const mockLocation = vi.fn() + +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useLocation: () => mockLocation, + } +}) + const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -22,17 +35,20 @@ describe('SlotInformation', () => { beforeEach(() => { props = { + robotType: FLEX_ROBOT_TYPE, location: 'A1', liquids: [], labwares: [], + adapters: [], modules: [], + fixtures: [], } }) it('should render DeckInfoLabel and title', () => { render(props) screen.getByText('A1') - screen.getByText('Slot Stack Information') + screen.getByText('Slot Detail') }) it('should render liquid, labware, and module', () => { @@ -40,7 +56,8 @@ describe('SlotInformation', () => { screen.getByText('Liquid') screen.getByText('Labware') screen.getByText('Module') - expect(screen.getAllByText('None').length).toBe(3) + screen.getByText('Fixtures') + expect(screen.getAllByText('None').length).toBe(4) }) it('should render info of liquid, labware, and module', () => { @@ -48,15 +65,18 @@ describe('SlotInformation', () => { ...props, liquids: mockLiquids, labwares: mockLabwares, + adapters: mockAdapters, modules: mockModules, } render(props) - expect(screen.getAllByText('Liquid').length).toBe(mockLiquids.length) - expect(screen.getAllByText('Labware').length).toBe(mockLabwares.length) + screen.debug() + + expect(screen.getAllByText('Liquid').length).toBe(1) + expect(screen.getAllByText('Labware').length).toBe( + mockLabwares.length + mockAdapters.length + ) expect(screen.getAllByText('Module').length).toBe(mockModules.length) - screen.getByText('Mastermix') - screen.getByText('Ethanol') - screen.getByText('Water') + screen.getByText('Mastermix, Ethanol, Water') screen.getByText('96 Well Plate') screen.getByText('Adapter') screen.getByText('Thermocycler Module Gen2') diff --git a/protocol-designer/src/organisms/SlotInformation/index.tsx b/protocol-designer/src/organisms/SlotInformation/index.tsx index cdb8d2f3d94..c01c1266cc5 100644 --- a/protocol-designer/src/organisms/SlotInformation/index.tsx +++ b/protocol-designer/src/organisms/SlotInformation/index.tsx @@ -1,5 +1,6 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { useLocation } from 'react-router-dom' import { ALIGN_CENTER, DeckInfoLabel, @@ -10,21 +11,30 @@ import { SPACING, StyledText, } from '@opentrons/components' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import type { RobotType } from '@opentrons/shared-data' interface SlotInformationProps { location: string + robotType: RobotType liquids?: string[] labwares?: string[] + adapters?: string[] modules?: string[] + fixtures?: string[] } export const SlotInformation: React.FC = ({ location, + robotType, liquids = [], labwares = [], + adapters = [], modules = [], + fixtures = [], }) => { const { t } = useTranslation('shared') + const isOffDeck = location === 'offDeck' return ( = ({ width="100%" > - + {isOffDeck ? null : } - {t('slot_stack_information')} + {t(isOffDeck ? 'labware_detail' : 'slot_detail')} - + {liquids.length > 1 ? ( + + + + ) : ( + + )} - + {adapters.length > 0 ? ( + + ) : null} + {isOffDeck ? null : ( + + )} + {robotType === FLEX_ROBOT_TYPE && !isOffDeck ? ( + + ) : null} ) @@ -52,8 +80,13 @@ interface StackInfoListProps { } function StackInfoList({ title, items }: StackInfoListProps): JSX.Element { + const pathLocation = useLocation() return ( - <> + {items.length > 0 ? ( items.map((item, index) => ( )} - + ) } diff --git a/protocol-designer/src/organisms/index.ts b/protocol-designer/src/organisms/index.ts index 246fa988a8c..f2ba204e3bb 100644 --- a/protocol-designer/src/organisms/index.ts +++ b/protocol-designer/src/organisms/index.ts @@ -1,2 +1,14 @@ +export * from './AnnouncementModal' +export * from './AssignLiquidsModal' +export * from './DefineLiquidsModal' +export * from './EditInstrumentsModal' +export * from './EditNickNameModal' +export * from './EditProtocolMetadataModal' +export * from './FileUploadMessagesModal/' +export * from './IncompatibleTipsModal' export * from './Kitchen' +export * from './LabwareUploadModal' +export * from './PipetteInfoItem' +export * from './ProtocolMetadataNav' +export * from './SlotDetailsContainer' export * from './SlotInformation' diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx index 08013bade99..da621add9f4 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx @@ -12,9 +12,12 @@ import { } from '@opentrons/components' import { InputField } from '../../components/modals/CreateFileWizard/InputField' import { WizardBody } from './WizardBody' +import { HandleEnter } from './HandleEnter' import type { WizardTileProps } from './types' +const FLEX_METADATA_WIZARD_STEP = 6 +const OT2_METADATA_WIZARD_STEP = 4 export function AddMetadata(props: WizardTileProps): JSX.Element | null { const { goBack, proceed, watch, register } = props const { t } = useTranslation(['create_new_protocol', 'shared']) @@ -22,45 +25,51 @@ export function AddMetadata(props: WizardTileProps): JSX.Element | null { const robotType = fields.robotType return ( - { - goBack(1) - }} - proceed={() => { - proceed(1) - }} - > - + { + goBack(1) + }} + proceed={() => { + proceed(1) + }} > - - {t('name')} - {/* TODO(ja, 8/9/24): add new input field */} - + + + {t('name')} + {/* TODO(ja, 8/9/24): add new input field */} + + + + + {t('description')} + + + + + + {t('author_org')} + + {/* TODO(ja, 8/9/24): add new input field */} + + - - - {t('description')} - - - - - - {t('author_org')} - - {/* TODO(ja, 8/9/24): add new input field */} - - - - + + ) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/HandleEnter.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/HandleEnter.tsx new file mode 100644 index 00000000000..a4d07b43a0e --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/HandleEnter.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import { HandleKeypress } from '@opentrons/components' + +interface HandleEnterProps { + children: React.ReactNode + onEnter: () => void +} + +export function HandleEnter(props: HandleEnterProps): JSX.Element { + const { children, onEnter } = props + + return ( + + {children} + + ) +} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 552a06f67dd..7a018ec7e64 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import without from 'lodash/without' +import { THERMOCYCLER_MODULE_V2 } from '@opentrons/shared-data' import { DIRECTION_COLUMN, EmptySelectorButton, @@ -12,97 +13,171 @@ import { TYPOGRAPHY, WRAP, } from '@opentrons/components' +import { useKitchen } from '../../organisms/Kitchen/hooks' import { WizardBody } from './WizardBody' -import { AdditionalEquipmentDiagram } from './utils' +import { + AdditionalEquipmentDiagram, + getNumOptions, + getNumSlotsAvailable, +} from './utils' +import { HandleEnter } from './HandleEnter' +import type { DropdownBorder } from '@opentrons/components' import type { AdditionalEquipment, WizardTileProps } from './types' +const MAX_SLOTS = 4 const ADDITIONAL_EQUIPMENTS: AdditionalEquipment[] = [ 'wasteChute', 'trashBin', - 'stagingArea_cutoutA3', - 'stagingArea_cutoutB3', - 'stagingArea_cutoutC3', - 'stagingArea_cutoutD3', + 'stagingArea', ] export function SelectFixtures(props: WizardTileProps): JSX.Element | null { const { goBack, proceed, setValue, watch } = props + const { makeSnackbar } = useKitchen() const additionalEquipment = watch('additionalEquipment') + const modules = watch('modules') const { t } = useTranslation(['create_new_protocol', 'shared']) + const numSlotsAvailable = getNumSlotsAvailable(modules, additionalEquipment) + + const hasTC = + modules != null && + Object.values(modules).some( + module => module.model === THERMOCYCLER_MODULE_V2 + ) + const hasTrash = additionalEquipment.some( + ae => ae === 'trashBin' || ae === 'wasteChute' + ) const filteredAdditionalEquipmentWithoutGripper = additionalEquipment.filter( ae => ae !== 'gripper' ) + const filteredDuplicateStagingAreas = Array.from( + new Set(filteredAdditionalEquipmentWithoutGripper) + ) const filteredAdditionalEquipment = ADDITIONAL_EQUIPMENTS.filter( equipment => !filteredAdditionalEquipmentWithoutGripper.includes(equipment) ) + const handleProceed = (): void => { + if (!hasTrash) { + makeSnackbar(t('trash_required') as string) + } else { + proceed(1) + } + } + return ( - { - goBack(1) - }} - proceed={() => { - proceed(1) - }} - > - - - - {t('which_fixtures')} - - - - {filteredAdditionalEquipment.map(equipment => ( - { - setValue('additionalEquipment', [ - ...additionalEquipment, - equipment, - ]) - }} - /> - ))} - - - - {t('fixtures_added')} - - - {filteredAdditionalEquipmentWithoutGripper.map(ae => ( - - { - setValue( - 'additionalEquipment', - without(additionalEquipment, ae) - ) - }} - header={t(`${ae}`)} - leftHeaderItem={ - + + { + goBack(1) + }} + proceed={handleProceed} + > + + + + {t('which_fixtures')} + + + + {filteredAdditionalEquipment.map(equipment => ( + { + if (numSlotsAvailable === 0) { + makeSnackbar(t('slots_limit_reached') as string) + } else { + setValue('additionalEquipment', [ + ...additionalEquipment, + equipment, + ]) } - /> - + }} + /> ))} + + + {t('fixtures_added')} + + + {filteredDuplicateStagingAreas.map(ae => { + const numStagingAreas = filteredAdditionalEquipmentWithoutGripper.filter( + additionalEquipment => additionalEquipment === 'stagingArea' + )?.length + + const dropdownProps = { + currentOption: { + name: numStagingAreas.toString(), + value: numStagingAreas.toString(), + }, + dropdownType: 'neutral' as DropdownBorder, + filterOptions: getNumOptions( + numSlotsAvailable >= MAX_SLOTS + ? MAX_SLOTS + : numSlotsAvailable + numStagingAreas - (hasTC ? 1 : 0) + ), + onClick: (value: string) => { + const inputNum = parseInt(value) + let updatedStagingAreas = [...additionalEquipment] + + if (inputNum > numStagingAreas) { + const difference = inputNum - numStagingAreas + updatedStagingAreas = [ + ...updatedStagingAreas, + ...Array(difference).fill(ae), + ] + } else { + updatedStagingAreas = updatedStagingAreas.slice( + 0, + inputNum + ) + } + + setValue('additionalEquipment', updatedStagingAreas) + }, + } + return ( + + { + setValue( + 'additionalEquipment', + without(additionalEquipment, ae) + ) + }} + label={ae === 'stagingArea' ? t('quantity') : null} + dropdown={ + ae === 'stagingArea' ? dropdownProps : undefined + } + header={t(`${ae}`)} + leftHeaderItem={ + + } + /> + + ) + })} + + - - + + ) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectGripper.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectGripper.tsx index 6185ae24cf4..fcbb8a47d4c 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectGripper.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectGripper.tsx @@ -9,6 +9,7 @@ import { DIRECTION_COLUMN, } from '@opentrons/components' import { WizardBody } from './WizardBody' +import { HandleEnter } from './HandleEnter' import type { WizardTileProps } from './types' @@ -31,44 +32,51 @@ export function SelectGripper(props: WizardTileProps): JSX.Element | null { } } + const isDisabled = gripperStatus == null + const handleProceed = (): void => { + if (!isDisabled) { + proceed(1) + } + } + return ( - { - goBack(1) - }} - proceed={() => { - proceed(1) - }} - > - - - {t('need_gripper')} - - - { - handleGripperSelection('yes') - }} - buttonLabel={t('shared:yes')} - buttonValue="yes" - isSelected={gripperStatus === 'yes'} - /> - { - handleGripperSelection('no') - }} - buttonLabel={t('shared:no')} - buttonValue="no" - isSelected={gripperStatus === 'no'} - /> + + { + goBack(1) + }} + proceed={handleProceed} + > + + + {t('need_gripper')} + + + { + handleGripperSelection('yes') + }} + buttonLabel={t('shared:yes')} + buttonValue="yes" + isSelected={gripperStatus === 'yes'} + /> + { + handleGripperSelection('no') + }} + buttonLabel={t('shared:no')} + buttonValue="no" + isSelected={gripperStatus === 'no'} + /> + - - + + ) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index c40425daa13..012be6026c3 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -19,41 +19,35 @@ import { getModuleType, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, + MAGNETIC_BLOCK_V1, TEMPERATURE_MODULE_TYPE, - THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import { uuid } from '../../utils' import { getEnableAbsorbanceReader, getEnableMoam, } from '../../feature-flags/selectors' +import { useKitchen } from '../../organisms/Kitchen/hooks' import { ModuleDiagram } from '../../components/modules' import { WizardBody } from './WizardBody' import { DEFAULT_SLOT_MAP_FLEX, DEFAULT_SLOT_MAP_OT2, FLEX_SUPPORTED_MODULE_MODELS, - MAX_MAGNETIC_BLOCKS, - MAX_MOAM_MODULES, OT2_SUPPORTED_MODULE_MODELS, } from './constants' -import { getNumSlotsAvailable } from './utils' +import { getNumOptions, getNumSlotsAvailable } from './utils' +import { HandleEnter } from './HandleEnter' -import type { DropdownOption } from '@opentrons/components' +import type { DropdownBorder } from '@opentrons/components' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' import type { FormModules } from '../../step-forms' import type { WizardTileProps } from './types' -const getMoamOptions = (length: number): DropdownOption[] => { - return Array.from({ length }, (_, i) => ({ - name: `${i + 1}`, - value: `${i + 1}`, - })) -} - export function SelectModules(props: WizardTileProps): JSX.Element | null { const { goBack, proceed, watch, setValue } = props const { t } = useTranslation(['create_new_protocol', 'shared']) + const { makeSnackbar } = useKitchen() const fields = watch('fields') const modules = watch('modules') const additionalEquipment = watch('additionalEquipment') @@ -65,6 +59,14 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { ? FLEX_SUPPORTED_MODULE_MODELS : OT2_SUPPORTED_MODULE_MODELS + const numSlotsAvailable = getNumSlotsAvailable(modules, additionalEquipment) + const hasNoAvailableSlots = numSlotsAvailable === 0 + const numMagneticBlocks = + modules != null + ? Object.values(modules).filter( + module => module.model === MAGNETIC_BLOCK_V1 + )?.length + : 0 const filteredSupportedModules = supportedModules.filter( moduleModel => !( @@ -80,17 +82,6 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { ? [TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE] : [TEMPERATURE_MODULE_TYPE] - let isDisabled = getNumSlotsAvailable(modules, additionalEquipment) === 0 - // special-casing TC since it takes up 2 slots - if ( - modules != null && - Object.values(modules).some( - module => module.type === THERMOCYCLER_MODULE_TYPE - ) - ) { - isDisabled = getNumSlotsAvailable(modules, additionalEquipment) <= 1 - } - const filteredModules: FormModules = {} const seenModels = new Set() @@ -104,162 +95,188 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { } return ( - { - goBack(1) - setValue('modules', null) - }} - proceed={() => { - proceed(1) - }} - > - - - - {t('which_mods')} - - - {filteredSupportedModules - .filter( - module => - module !== ABSORBANCE_READER_V1 && enableAbsorbanceReader - ) - .map(moduleModel => ( - { - setValue('modules', { - ...modules, - [uuid()]: { - model: moduleModel, - type: getModuleType(moduleModel), - slot: - robotType === FLEX_ROBOT_TYPE - ? DEFAULT_SLOT_MAP_FLEX[moduleModel] - : DEFAULT_SLOT_MAP_OT2[getModuleType(moduleModel)], - }, - }) - }} - /> - ))} - - {modules != null && - Object.keys(modules).length > 0 && - Object.keys(filteredModules).length > 0 ? ( - + + { + goBack(1) + setValue('modules', null) + }} + proceed={() => { + proceed(1) + }} + > + + + {filteredSupportedModules.length > 0 ? ( - {t('modules_added')} + {t('which_mods')} - - {Object.values(filteredModules).map((module, index) => { - const length = Object.values(modules).filter( - mod => module.type === mod.type - ).length + ) : null} + + {filteredSupportedModules + .filter(module => + enableAbsorbanceReader + ? module + : module !== ABSORBANCE_READER_V1 + ) + .map(moduleModel => ( + { + if (hasNoAvailableSlots) { + makeSnackbar(t('slots_limit_reached') as string) + } else { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: getModuleType(moduleModel), + slot: + robotType === FLEX_ROBOT_TYPE + ? DEFAULT_SLOT_MAP_FLEX[moduleModel] + : DEFAULT_SLOT_MAP_OT2[ + getModuleType(moduleModel) + ], + }, + }) + } + }} + /> + ))} + + {modules != null && + Object.keys(modules).length > 0 && + Object.keys(filteredModules).length > 0 ? ( + + + {t('modules_added')} + + + {Object.values(filteredModules).map((module, index) => { + const length = Object.values(modules).filter( + mod => module.type === mod.type + ).length - const dropdownProps = { - currentOption: { name: `${length}`, value: `${length}` }, - onClick: (value: string) => { - const num = parseInt(value) - const moamModules = - modules != null - ? Object.entries(modules).filter( - ([key, mod]) => mod.type === module.type - ) - : [] + const dropdownProps = { + currentOption: { name: `${length}`, value: `${length}` }, + onClick: (value: string) => { + const num = parseInt(value) + const moamModules = + modules != null + ? Object.entries(modules).filter( + ([key, mod]) => mod.type === module.type + ) + : [] - if (num > moamModules.length) { - const newModules = { ...modules } - for (let i = 0; i < num - moamModules.length; i++) { - // @ts-expect-error: TS can't determine modules's type correctly - newModules[uuid()] = { - model: module.model, - type: module.type, - slot: null, + if (num > moamModules.length) { + const newModules = { ...modules } + for (let i = 0; i < num - moamModules.length; i++) { + // @ts-expect-error: TS can't determine modules's type correctly + newModules[uuid()] = { + model: module.model, + type: module.type, + slot: null, + } } - } - setValue('modules', newModules) - } else if (num < moamModules.length) { - const modulesToRemove = moamModules.length - num - const remainingModules: FormModules = {} + setValue('modules', newModules) + } else if (num < moamModules.length) { + const modulesToRemove = moamModules.length - num + const remainingModules: FormModules = {} - Object.entries(modules).forEach(([key, mod]) => { - const shouldRemove = moamModules - .slice(-modulesToRemove) - .some(([removeKey]) => removeKey === key) - if (!shouldRemove) { - remainingModules[parseInt(key)] = mod - } - }) + Object.entries(modules).forEach(([key, mod]) => { + const shouldRemove = moamModules + .slice(-modulesToRemove) + .some(([removeKey]) => removeKey === key) + if (!shouldRemove) { + remainingModules[parseInt(key)] = mod + } + }) - setValue('modules', remainingModules) - } - }, - dropdownType: 'neutral' as any, - filterOptions: getMoamOptions( - module.type === MAGNETIC_BLOCK_TYPE - ? MAX_MAGNETIC_BLOCKS - : MAX_MOAM_MODULES - ), - } - return ( - - { - const updatedModules = - modules != null - ? Object.fromEntries( - Object.entries(modules).filter( - ([key, value]) => value.type !== module.type + }, + dropdownType: 'neutral' as DropdownBorder, + filterOptions: getNumOptions( + module.model === 'magneticBlockV1' + ? numSlotsAvailable + 3 + length + : numSlotsAvailable + length + ), + } + return ( + + { + const updatedModules = + modules != null + ? Object.fromEntries( + Object.entries(modules).filter( + ([key, value]) => + value.type !== module.type + ) ) - ) - : {} - setValue('modules', updatedModules) - }} - header={getModuleDisplayName(module.model)} - leftHeaderItem={ - - } - /> - - ) - })} + : {} + setValue('modules', updatedModules) + }} + header={getModuleDisplayName(module.model)} + leftHeaderItem={ + + } + /> + + ) + })} + - - ) : null} + ) : null} + - - + + ) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx index 464a0f71ee1..6f95166d268 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx @@ -13,27 +13,34 @@ import { Btn, Checkbox, DIRECTION_COLUMN, + DIRECTION_ROW, + DISPLAY_FLEX, + DISPLAY_INLINE_BLOCK, EmptySelectorButton, Flex, + Icon, JUSTIFY_SPACE_BETWEEN, PRODUCT, RadioButton, SPACING, StyledText, TYPOGRAPHY, + WRAP, } from '@opentrons/components' import { getAllowAllTipracks } from '../../feature-flags/selectors' import { getLabwareDefsByURI } from '../../labware-defs/selectors' -import { getTiprackOptions } from '../../components/modals/utils' import { setFeatureFlags } from '../../feature-flags/actions' import { createCustomTiprackDef } from '../../labware-defs/actions' import { useKitchen } from '../../organisms/Kitchen/hooks' -import { PipetteInfoItem } from './PipetteInfoItem' +import { IncompatibleTipsModal, PipetteInfoItem } from '../../organisms' +import { BUTTON_LINK_STYLE } from '../../atoms' import { WizardBody } from './WizardBody' import { PIPETTE_GENS, PIPETTE_TYPES, PIPETTE_VOLUMES } from './constants' +import { getTiprackOptions } from './utils' +import { HandleEnter } from './HandleEnter' -import type { PipetteMount, PipetteName } from '@opentrons/shared-data' import type { ThunkDispatch } from 'redux-thunk' +import type { PipetteMount, PipetteName } from '@opentrons/shared-data' import type { BaseState } from '../../types' import type { Gen, @@ -54,11 +61,14 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { const [mount, setMount] = React.useState(null) const [page, setPage] = React.useState<'add' | 'overview'>('add') const [pipetteType, setPipetteType] = React.useState(null) + const [showIncompatibleTip, setIncompatibleTip] = React.useState( + false + ) const [pipetteGen, setPipetteGen] = React.useState('flex') const [pipetteVolume, setPipetteVolume] = React.useState(null) const allowAllTipracks = useSelector(getAllowAllTipracks) const allPipetteOptions = getAllPipetteNames('maxVolume', 'channels') - const robotType = fields.robotType ?? OT2_ROBOT_TYPE + const robotType = fields.robotType const defaultMount = mount ?? 'left' const has96Channel = pipettesByMount.left.pipetteName === 'p1000_96' const selectedPip = @@ -74,348 +84,416 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { setPipetteVolume(null) } - // initialize pipette name once all fields are filled out + // initialize pipette name once all fields are filled out React.useEffect(() => { if (pipetteType != null && pipetteVolume != null) { setValue(`pipettesByMount.${defaultMount}.pipetteName`, selectedPip) } }, [pipetteType, pipetteGen, pipetteVolume, selectedPip]) - return ( - { - if (page === 'overview') { - proceed(1) - } else { - setPage('overview') - } - }} - goBack={() => { - if (page === 'add') { - resetFields() - setValue(`pipettesByMount.${defaultMount}.pipetteName`, undefined) - setValue(`pipettesByMount.${defaultMount}.tiprackDefURI`, undefined) - goBack(1) - } else { - setPage('add') - } - }} - disabled={ - page === 'add' && pipettesByMount[defaultMount].tiprackDefURI == null + const isDisabled = + page === 'add' && pipettesByMount[defaultMount].tiprackDefURI == null + + const handleProceed = (): void => { + if (!isDisabled) { + if (page === 'overview') { + proceed(1) + } else { + setPage('overview') } - > - {page === 'add' ? ( - + {showIncompatibleTip ? ( + { + setIncompatibleTip(false) + }} + /> + ) : null} + + { + if (page === 'add') { + resetFields() + setValue(`pipettesByMount.${defaultMount}.pipetteName`, undefined) + setValue( + `pipettesByMount.${defaultMount}.tiprackDefURI`, + undefined + ) + goBack(1) + } else { + setPage('add') + } + }} + disabled={isDisabled} > - <> - - {t('pip_type')} - - - {PIPETTE_TYPES[robotType].map(type => { - return type.value === '96' && - (pipettesByMount.left.pipetteName != null || - pipettesByMount.right.pipetteName != null) ? null : ( - { - setPipetteType(type.value) - setPipetteGen('flex') - setPipetteVolume(null) - setValue( - `pipettesByMount.${defaultMount}.pipetteName`, - undefined - ) - setValue( - `pipettesByMount.${defaultMount}.tiprackDefURI`, - undefined - ) - }} - buttonLabel={t(`shared:${type.label}`)} - buttonValue="single" - isSelected={pipetteType === type.value} - /> - ) - })} - - - {pipetteType != null && robotType === OT2_ROBOT_TYPE ? ( - - - {t('pip_gen')} - - - {PIPETTE_GENS.map(gen => ( - { - setPipetteGen(gen) - setPipetteVolume(null) - }} - buttonLabel={gen} - buttonValue={gen} - isSelected={pipetteGen === gen} - /> - ))} - - - ) : null} - {(pipetteType != null && robotType === FLEX_ROBOT_TYPE) || - (pipetteGen !== 'flex' && - pipetteType != null && - robotType === OT2_ROBOT_TYPE) ? ( + {page === 'add' ? ( - - {t('pip_vol')} - - - {PIPETTE_VOLUMES[robotType]?.map(volume => { - if (robotType === FLEX_ROBOT_TYPE && pipetteType != null) { - const flexVolume = volume as PipetteInfoByType - const flexPipetteInfo = flexVolume[pipetteType] - - return flexPipetteInfo?.map(type => ( + <> + + {t('pip_type')} + + + {PIPETTE_TYPES[robotType].map(type => { + return type.value === '96' && + (pipettesByMount.left.pipetteName != null || + pipettesByMount.right.pipetteName != null) ? null : ( { - setPipetteVolume(type.value) + setPipetteType(type.value) + setPipetteGen('flex') + setPipetteVolume(null) + setValue( + `pipettesByMount.${defaultMount}.pipetteName`, + undefined + ) + setValue( + `pipettesByMount.${defaultMount}.tiprackDefURI`, + undefined + ) }} - buttonLabel={t('vol_label', { volume: type.label })} - buttonValue={type.value} - isSelected={pipetteVolume === type.value} + buttonLabel={t(`shared:${type.label}`)} + buttonValue="single" + isSelected={pipetteType === type.value} /> - )) - } else { - const ot2Volume = volume as PipetteInfoByGen - // asserting gen is defined from previous turnary statement - const gen = pipetteGen as Gen - - return ot2Volume[gen].map(info => { - return info[pipetteType]?.map(type => ( - { - setPipetteVolume(type.value) - }} - buttonLabel={t('vol_label', { volume: type.label })} - buttonValue={type.value} - isSelected={pipetteVolume === type.value} - /> - )) - }) - } - })} - - - ) : null} - {allPipetteOptions.includes(selectedPip as PipetteName) - ? (() => { - const tiprackOptions = getTiprackOptions({ - allLabware: allLabware, - allowAllTipracks: allowAllTipracks, - selectedPipetteName: selectedPip, - }) - return ( - + + {pipetteType != null && robotType === OT2_ROBOT_TYPE ? ( + + - - {t('pip_tips')} - - - {tiprackOptions.map(option => ( - { - const updatedValues = selectedValues?.includes( - option.value - ) - ? selectedValues.filter( - value => value !== option.value - ) - : [...(selectedValues ?? []), option.value] - setValue( - `pipettesByMount.${defaultMount}.tiprackDefURI`, - updatedValues.slice(0, 3) - ) - if (selectedValues.length === 3) { - makeSnackbar(t('up_to_3_tipracks') as string) - } - }} - /> - ))} - - - - - {t('add_custom_tips')} - - dispatch(createCustomTiprackDef(e))} - /> - - { - dispatch( - setFeatureFlags({ - OT_PD_ALLOW_ALL_TIPRACKS: !allowAllTipracks, - }) - ) + {t('pip_gen')} + + + {PIPETTE_GENS.map(gen => ( + { + setPipetteGen(gen) + setPipetteVolume(null) }} - textDecoration={TYPOGRAPHY.textDecorationUnderline} - > - - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} - - - + buttonLabel={gen} + buttonValue={gen} + isSelected={pipetteGen === gen} + /> + ))} - ) - })() - : null} - - ) : ( - - - - {t('your_pips')} - - {has96Channel ? null : ( - { - const leftPipetteName = pipettesByMount.left.pipetteName - const rightPipetteName = pipettesByMount.right.pipetteName - const leftTiprackDefURI = pipettesByMount.left.tiprackDefURI - const rightTiprackDefURI = pipettesByMount.right.tiprackDefURI + + ) : null} + {(pipetteType != null && robotType === FLEX_ROBOT_TYPE) || + (pipetteGen !== 'flex' && + pipetteType != null && + robotType === OT2_ROBOT_TYPE) ? ( + + + {t('pip_vol')} + + + {PIPETTE_VOLUMES[robotType]?.map(volume => { + if ( + robotType === FLEX_ROBOT_TYPE && + pipetteType != null + ) { + const flexVolume = volume as PipetteInfoByType + const flexPipetteInfo = flexVolume[pipetteType] + + return flexPipetteInfo?.map(type => ( + { + setPipetteVolume(type.value) + }} + buttonLabel={t('vol_label', { volume: type.label })} + buttonValue={type.value} + isSelected={pipetteVolume === type.value} + /> + )) + } else { + const ot2Volume = volume as PipetteInfoByGen + // asserting gen is defined from previous turnary statement + const gen = pipetteGen as Gen - setValue('pipettesByMount.left.pipetteName', rightPipetteName) - setValue('pipettesByMount.right.pipetteName', leftPipetteName) - setValue( - 'pipettesByMount.left.tiprackDefURI', - rightTiprackDefURI - ) - setValue( - 'pipettesByMount.right.tiprackDefURI', - leftTiprackDefURI - ) - }} + return ot2Volume[gen].map(info => { + return info[pipetteType]?.map(type => ( + { + setPipetteVolume(type.value) + }} + buttonLabel={t('vol_label', { + volume: type.label, + })} + buttonValue={type.value} + isSelected={pipetteVolume === type.value} + /> + )) + }) + } + })} + + + ) : null} + {allPipetteOptions.includes(selectedPip as PipetteName) + ? (() => { + const tiprackOptions = getTiprackOptions({ + allLabware: allLabware, + allowAllTipracks: allowAllTipracks, + selectedPipetteName: selectedPip, + }) + return ( + + + {t('pip_tips')} + + + {Object.entries(tiprackOptions).map( + ([value, name]) => ( + { + const updatedValues = selectedValues.includes( + value + ) + ? selectedValues.filter(v => v !== value) + : [...selectedValues, value] + setValue( + `pipettesByMount.${defaultMount}.tiprackDefURI`, + updatedValues.slice(0, 3) + ) + if (selectedValues.length === 3) { + makeSnackbar( + t('up_to_3_tipracks') as string + ) + } + }} + /> + ) + )} + + + + + {t('add_custom_tips')} + + + dispatch(createCustomTiprackDef(e)) + } + /> + + {pipetteVolume === 'p1000' && + robotType === FLEX_ROBOT_TYPE ? null : ( + { + if (allowAllTipracks) { + dispatch( + setFeatureFlags({ + OT_PD_ALLOW_ALL_TIPRACKS: !allowAllTipracks, + }) + ) + } else { + setIncompatibleTip(true) + } + }} + textDecoration={ + TYPOGRAPHY.textDecorationUnderline + } + > + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + + )} + + + ) + })() + : null} + + ) : ( + + - - {t('swap')} + + {t('your_pips')} - - )} - - - {pipettesByMount.left.pipetteName != null && - pipettesByMount.left.tiprackDefURI != null ? ( - { - setPage('add') - setMount('left') - }} - setValue={setValue} - cleanForm={resetFields} - watch={watch} - /> - ) : ( - { - setPage('add') - setMount('left') - resetFields() - }} - text={t('add_pip')} - textAlignment="left" - iconName="plus" - size="large" - /> - )} - {pipettesByMount.right.pipetteName != null && - pipettesByMount.right.tiprackDefURI != null ? ( - { - setPage('add') - setMount('right') - }} - setValue={setValue} - cleanForm={resetFields} - /> - ) : has96Channel ? null : ( - { - setPage('add') - setMount('right') - resetFields() - }} - text={t('add_pip')} - textAlignment="left" - iconName="plus" - size="large" - /> - )} - - - )} - + {has96Channel ? null : ( + { + const leftPipetteName = pipettesByMount.left.pipetteName + const rightPipetteName = pipettesByMount.right.pipetteName + const leftTiprackDefURI = + pipettesByMount.left.tiprackDefURI + const rightTiprackDefURI = + pipettesByMount.right.tiprackDefURI + + setValue( + 'pipettesByMount.left.pipetteName', + rightPipetteName + ) + setValue( + 'pipettesByMount.right.pipetteName', + leftPipetteName + ) + setValue( + 'pipettesByMount.left.tiprackDefURI', + rightTiprackDefURI + ) + setValue( + 'pipettesByMount.right.tiprackDefURI', + leftTiprackDefURI + ) + }} + > + + + + {t('swap')} + + + + )} + + + {pipettesByMount.left.pipetteName != null && + pipettesByMount.left.tiprackDefURI != null ? ( + { + setPage('add') + setMount('left') + }} + cleanForm={() => { + setValue(`pipettesByMount.left.pipetteName`, undefined) + setValue(`pipettesByMount.left.tiprackDefURI`, undefined) + + resetFields() + }} + /> + ) : ( + { + setPage('add') + setMount('left') + resetFields() + }} + text={t('add_pip')} + textAlignment="left" + iconName="plus" + size="large" + /> + )} + {pipettesByMount.right.pipetteName != null && + pipettesByMount.right.tiprackDefURI != null ? ( + { + setPage('add') + setMount('right') + }} + cleanForm={() => { + setValue(`pipettesByMount.right.pipetteName`, undefined) + setValue(`pipettesByMount.right.tiprackDefURI`, undefined) + resetFields() + }} + /> + ) : has96Channel ? null : ( + { + setPage('add') + setMount('right') + resetFields() + }} + text={t('add_pip')} + textAlignment="left" + iconName="plus" + size="large" + /> + )} + + + )} + + + ) } const StyledLabel = styled.label` text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; font-size: ${PRODUCT.TYPOGRAPHY.fontSizeBodyDefaultSemiBold}; - display: inline-block; + display: ${DISPLAY_INLINE_BLOCK}; cursor: pointer; input[type='file'] { display: none; diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectRobot.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectRobot.tsx index db08af44d53..68d9856e760 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectRobot.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectRobot.tsx @@ -9,6 +9,7 @@ import { } from '@opentrons/components' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { WizardBody } from './WizardBody' +import { HandleEnter } from './HandleEnter' import type { WizardTileProps } from './types' export function SelectRobot(props: WizardTileProps): JSX.Element { @@ -18,42 +19,43 @@ export function SelectRobot(props: WizardTileProps): JSX.Element { const robotType = fields?.robotType return ( - { - proceed(1) - }} - > - - - {t('robot_type')} - + + { + proceed(1) + }} + > + + + {t('robot_type')} + - - { - setValue('fields.robotType', FLEX_ROBOT_TYPE) - }} - buttonLabel={t('shared:opentrons_flex')} - buttonValue={FLEX_ROBOT_TYPE} - isSelected={robotType === FLEX_ROBOT_TYPE} - /> - { - setValue('fields.robotType', OT2_ROBOT_TYPE) - }} - buttonLabel={t('shared:ot2')} - buttonValue={OT2_ROBOT_TYPE} - isSelected={robotType === OT2_ROBOT_TYPE} - /> + + { + setValue('fields.robotType', FLEX_ROBOT_TYPE) + }} + buttonLabel={t('shared:opentrons_flex')} + buttonValue={FLEX_ROBOT_TYPE} + isSelected={robotType === FLEX_ROBOT_TYPE} + /> + { + setValue('fields.robotType', OT2_ROBOT_TYPE) + }} + buttonLabel={t('shared:ot2')} + buttonValue={OT2_ROBOT_TYPE} + isSelected={robotType === OT2_ROBOT_TYPE} + /> + - - + + ) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx index 4fbbf047220..498a9392685 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx @@ -15,13 +15,14 @@ import { JUSTIFY_SPACE_BETWEEN, } from '@opentrons/components' import temporaryImg from '../../assets/images/placeholder_image_delete.png' +import { BUTTON_LINK_STYLE } from '../../atoms' interface WizardBodyProps { stepNumber: number header: string children: React.ReactNode proceed: () => void - disabled: boolean + disabled?: boolean goBack?: () => void subHeader?: string imgSrc?: string @@ -34,13 +35,17 @@ export function WizardBody(props: WizardBodyProps): JSX.Element { goBack, subHeader, proceed, - disabled, + disabled = false, imgSrc, } = props const { t } = useTranslation('shared') return ( - + {goBack != null ? ( - - + + {t('go_back')} ) : null} ) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const values = { + fields: { + name: '', + description: '', + organizationOrAuthor: '', + robotType: FLEX_ROBOT_TYPE, + }, + additionalEquipment: ['trashBin'], + modules: {}, + pipettesByMount: {} as any, +} as WizardFormState + +const mockWizardTileProps: Partial = { + proceed: vi.fn(), + setValue: vi.fn(), + goBack: vi.fn(), + watch: vi.fn((name: keyof typeof values) => values[name]) as any, +} + +describe('SelectFixtures', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + ...props, + ...mockWizardTileProps, + } as WizardTileProps + }) + + it('renders the trash bin by default and all the default text', () => { + render(props) + screen.getByText('Step 5') + screen.getByText('Add your fixtures') + screen.getByText( + 'Fixtures replace standard deck slots and let you add functionality to your Flex.' + ) + screen.getByText('Staging area') + screen.getByText('Waste Chute') + screen.getByText('Which fixtures will you be using?') + screen.getByText('Fixtures added') + screen.getByText('Trash Bin') + }) + it('calls setValue when clicking to add a fixture', () => { + render(props) + fireEvent.click(screen.getByText('Staging area')) + expect(props.setValue).toHaveBeenCalled() + }) + it('calls goBack when clicking on go back', () => { + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + expect(props.goBack).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx new file mode 100644 index 00000000000..24a13089b0e --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx @@ -0,0 +1,101 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { + getEnableAbsorbanceReader, + getEnableMoam, +} from '../../../feature-flags/selectors' +import { renderWithProviders } from '../../../__testing-utils__' +import { SelectModules } from '../SelectModules' +import type { WizardFormState, WizardTileProps } from '../types' + +vi.mock('../../../feature-flags/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const values = { + fields: { + name: '', + description: '', + organizationOrAuthor: '', + robotType: FLEX_ROBOT_TYPE, + }, + additionalEquipment: ['trashBin'], + modules: {}, + pipettesByMount: {} as any, +} as WizardFormState + +const mockWizardTileProps: Partial = { + proceed: vi.fn(), + setValue: vi.fn(), + goBack: vi.fn(), + watch: vi.fn((name: keyof typeof values) => values[name]) as any, +} + +describe('SelectModules', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + ...mockWizardTileProps, + } as WizardTileProps + vi.mocked(getEnableMoam).mockReturnValue(true) + vi.mocked(getEnableAbsorbanceReader).mockReturnValue(true) + }) + + it('renders the flex options and overall text', () => { + render(props) + screen.getByText('Step 4') + screen.getByText('Add your modules') + screen.getByText('Select modules to use in your protocol.') + screen.getByText('Temperature Module GEN2') + screen.getByText('Heater-Shaker Module GEN1') + screen.getByText('Thermocycler Module GEN2') + screen.getByText('Magnetic Block GEN1') + }) + + it('renders the ot-2 options', () => { + const values = { + fields: { + name: '', + description: '', + organizationOrAuthor: '', + robotType: OT2_ROBOT_TYPE, + }, + additionalEquipment: ['trashBin'], + modules: {}, + pipettesByMount: {} as any, + } as WizardFormState + props = { + ...props, + watch: vi.fn((name: keyof typeof values) => values[name]) as any, + } + render(props) + screen.getByText('Temperature Module GEN2') + screen.getByText('Temperature Module GEN1') + screen.getByText('Heater-Shaker Module GEN1') + screen.getByText('Magnetic Module GEN2') + screen.getByText('Magnetic Module GEN1') + screen.getByText('Thermocycler Module GEN2') + screen.getByText('Thermocycler Module GEN1') + }) + + it('calls setValue when clicking to add a Magnetic Block GEN1', () => { + render(props) + fireEvent.click(screen.getByText('Magnetic Block GEN1')) + expect(props.setValue).toHaveBeenCalled() + }) + + it('calls goBack when clicking on go back', () => { + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + expect(props.goBack).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectPipettes.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectPipettes.test.tsx new file mode 100644 index 00000000000..a3c2b4429ce --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectPipettes.test.tsx @@ -0,0 +1,147 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { getLabwareDefsByURI } from '../../../labware-defs/selectors' +import { getAllowAllTipracks } from '../../../feature-flags/selectors' +import { IncompatibleTipsModal } from '../../../organisms' +import { createCustomTiprackDef } from '../../../labware-defs/actions' +import { SelectPipettes } from '../SelectPipettes' +import { getTiprackOptions } from '../utils' + +import type { WizardFormState, WizardTileProps } from '../types' + +vi.mock('../../../labware-defs/selectors') +vi.mock('../../../feature-flags/selectors') +vi.mock('../../../organisms') +vi.mock('../../../labware-defs/actions') +vi.mock('../utils') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const values = { + additionalEquipment: [], + fields: { + name: '', + description: '', + organizationOrAuthor: '', + robotType: FLEX_ROBOT_TYPE, + }, + pipettesByMount: { left: {}, right: {} }, + modules: null, +} as WizardFormState + +const mockWizardTileProps: Partial = { + proceed: vi.fn(), + setValue: vi.fn(), + watch: vi.fn((name: keyof typeof values) => values[name]) as any, +} + +describe('SelectPipettes', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + ...props, + goBack: vi.fn(), + ...mockWizardTileProps, + } as WizardTileProps + vi.mocked(IncompatibleTipsModal).mockReturnValue( +
mock incompatible tips modal
+ ) + vi.mocked(getLabwareDefsByURI).mockReturnValue({}) + vi.mocked(getAllowAllTipracks).mockReturnValue(false) + vi.mocked(getTiprackOptions).mockReturnValue({ + 'opentrons/opentrons_flex_96_tiprack_200ul/1': '200uL Flex tipracks', + 'opentrons/opentrons_flex_96_tiprack_1000ul/1': '1000uL Flex tipracks', + }) + }) + + it('renders the first page of select pipettes for a Flex', () => { + render(props) + screen.getByText('Step 2') + screen.getByText('Add a pipette and tips') + screen.getByText( + 'Pick your first pipette. If you need a second pipette, you can add it next.' + ) + screen.getByText('Pipette type') + // select pip type + fireEvent.click(screen.getByRole('label', { name: '1-Channel' })) + screen.getByText('Pipette volume') + // select pip volume + fireEvent.click(screen.getByRole('label', { name: '1000 uL' })) + // select tip + screen.getByText('Add custom pipette tips') + screen.getByText('200uL Flex tipracks') + fireEvent.click(screen.getByText('1000uL Flex tipracks')) + + screen.getByRole('button', { name: 'Confirm' }) + + // go back + fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + expect(props.goBack).toHaveBeenCalled() + + fireEvent.click(screen.getByRole('button', { name: 'Confirm' })) + }) + + it('renders the first page of select pipettes for an ot-2', () => { + vi.mocked(getTiprackOptions).mockReturnValue({ + 'opentrons/opentrons_96_tiprack_10ul/1': '10uL tipracks', + 'opentrons/opentrons_96_tiprack_300ul/1': '300uL tipracks', + }) + + const values = { + additionalEquipment: [], + fields: { + name: '', + description: '', + organizationOrAuthor: '', + robotType: OT2_ROBOT_TYPE, + }, + pipettesByMount: { left: {}, right: {} }, + modules: null, + } as WizardFormState + + props = { + ...props, + watch: vi.fn((name: keyof typeof values) => values[name]) as any, + } + render(props) + screen.getByText('Step 2') + screen.getByText('Add a pipette and tips') + screen.getByText( + 'Pick your first pipette. If you need a second pipette, you can add it next.' + ) + screen.getByText('Pipette type') + // select pip type + fireEvent.click(screen.getByRole('label', { name: '1-Channel' })) + + screen.getByText('Pipette generation') + // select gen + fireEvent.click(screen.getByRole('label', { name: 'GEN2' })) + + screen.getByText('Pipette volume') + // select pip volume + fireEvent.click(screen.getByRole('label', { name: '20 uL' })) + // select tip + screen.getByText('Add custom pipette tips') + screen.getByText('10uL tipracks') + fireEvent.click(screen.getByText('300uL tipracks')) + screen.getByText('Add custom pipette tips') + + // add custom pipette tips + fireEvent.change(screen.getByTestId('SelectPipettes_customTipInput')) + expect(vi.mocked(createCustomTiprackDef)).toHaveBeenCalled() + + // change all tip setting + fireEvent.click(screen.getByText('Show all tips')) + screen.getByText('mock incompatible tips modal') + }) +}) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts new file mode 100644 index 00000000000..6ebef7c330d --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts @@ -0,0 +1,123 @@ +import { it, describe, expect } from 'vitest' +import { + FLEX_ROBOT_TYPE, + HEATERSHAKER_MODULE_TYPE, + HEATERSHAKER_MODULE_V1, + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V2, + THERMOCYCLER_MODULE_TYPE, + THERMOCYCLER_MODULE_V2, +} from '@opentrons/shared-data' +import { getNumSlotsAvailable, getTrashSlot } from '../utils' + +import type { AdditionalEquipment, WizardFormState } from '../types' +import type { FormPipettesByMount } from '../../../step-forms' + +let MOCK_FORM_STATE = { + fields: { + name: 'mockName', + description: 'mockDescription', + organizationOrAuthor: 'mockOrganizationOrAuthor', + robotType: FLEX_ROBOT_TYPE, + }, + pipettesByMount: { + left: { pipetteName: 'mockPipetteName', tiprackDefURI: ['mocktip'] }, + right: { pipetteName: null, tiprackDefURI: null }, + } as FormPipettesByMount, + modules: {}, + additionalEquipment: [], +} as WizardFormState + +describe('getNumSlotsAvailable', () => { + it('should return 8 when there are no modules or additional equipment', () => { + const result = getNumSlotsAvailable(null, []) + expect(result).toBe(8) + }) + it('should return 0 when there is a TC and 7 modules', () => { + const mockModules = { + 0: { + model: HEATERSHAKER_MODULE_V1, + type: HEATERSHAKER_MODULE_TYPE, + slot: 'D1', + }, + 1: { + model: TEMPERATURE_MODULE_V2, + type: TEMPERATURE_MODULE_TYPE, + slot: 'D3', + }, + 2: { + model: TEMPERATURE_MODULE_V2, + type: TEMPERATURE_MODULE_TYPE, + slot: 'C1', + }, + 3: { + model: TEMPERATURE_MODULE_V2, + type: TEMPERATURE_MODULE_TYPE, + slot: 'B3', + }, + 4: { + model: THERMOCYCLER_MODULE_V2, + type: THERMOCYCLER_MODULE_TYPE, + slot: 'B1', + }, + 5: { + model: TEMPERATURE_MODULE_V2, + type: TEMPERATURE_MODULE_TYPE, + + slot: 'A3', + }, + 6: { + model: TEMPERATURE_MODULE_V2, + type: TEMPERATURE_MODULE_TYPE, + slot: 'C3', + }, + } as any + const result = getNumSlotsAvailable(mockModules, []) + expect(result).toBe(0) + }) + it('should return 1 when there are 9 additional equipment and 1 is a waste chute on the staging area and one is a gripper', () => { + const mockAdditionalEquipment: AdditionalEquipment[] = [ + 'trashBin', + 'stagingArea', + 'stagingArea', + 'stagingArea', + 'stagingArea', + 'wasteChute', + 'trashBin', + 'gripper', + 'trashBin', + ] + const result = getNumSlotsAvailable(null, mockAdditionalEquipment) + expect(result).toBe(1) + }) + it('should return 8 even when there is a magnetic block', () => { + const mockModules = { + 0: { + model: 'magneticBlockV1', + type: 'magneticBlockType', + slot: 'B2', + }, + } as any + const result = getNumSlotsAvailable(mockModules, []) + expect(result).toBe(8) + }) +}) + +describe('getTrashSlot', () => { + it('should return the default slot A3 when there is no staging area or module in that slot', () => { + MOCK_FORM_STATE = { + ...MOCK_FORM_STATE, + additionalEquipment: ['trashBin'], + } + const result = getTrashSlot(MOCK_FORM_STATE) + expect(result).toBe('cutoutA3') + }) + it('should return cutoutA1 when there is a staging area in slot A3', () => { + MOCK_FORM_STATE = { + ...MOCK_FORM_STATE, + additionalEquipment: ['stagingArea'], + } + const result = getTrashSlot(MOCK_FORM_STATE) + expect(result).toBe('cutoutA1') + }) +}) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts index f4e8b800203..ae220daf450 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts @@ -103,11 +103,6 @@ export const PIPETTE_VOLUMES: PipetteVolumes = { ], } -export const MAX_MOAM_MODULES = 7 -// limiting 10 instead of 11 to make space for a single default tiprack -// to be auto-generated -export const MAX_MAGNETIC_BLOCKS = 10 - export const FLEX_SUPPORTED_MODULE_MODELS: ModuleModel[] = [ THERMOCYCLER_MODULE_V2, HEATERSHAKER_MODULE_V1, diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx index 1fe7321a47a..753965c6300 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx @@ -6,7 +6,6 @@ import uniq from 'lodash/uniq' import mapValues from 'lodash/mapValues' import { yupResolver } from '@hookform/resolvers/yup' import { useDispatch, useSelector } from 'react-redux' -import { useTranslation } from 'react-i18next' import { useForm } from 'react-hook-form' import { useNavigate } from 'react-router-dom' import { @@ -15,16 +14,14 @@ import { MAGNETIC_BLOCK_TYPE, MAGNETIC_MODULE_TYPE, OT2_ROBOT_TYPE, + STAGING_AREA_CUTOUTS, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, WASTE_CHUTE_CUTOUT, getAreSlotsAdjacent, } from '@opentrons/shared-data' import { Box, COLORS } from '@opentrons/components' -import { - actions as fileActions, - selectors as loadFileSelectors, -} from '../../load-file' +import { actions as fileActions } from '../../load-file' import { uuid } from '../../utils' import * as labwareDefSelectors from '../../labware-defs/selectors' import * as labwareDefActions from '../../labware-defs/actions' @@ -37,12 +34,14 @@ import { createDeckFixture, toggleIsGripperRequired, } from '../../step-forms/actions/additionalItems' +import { getNewProtocolModal } from '../../navigation/selectors' import { SelectRobot } from './SelectRobot' import { SelectPipettes } from './SelectPipettes' import { SelectGripper } from './SelectGripper' import { SelectModules } from './SelectModules' import { SelectFixtures } from './SelectFixtures' import { AddMetadata } from './AddMetadata' +import { getTrashSlot } from './utils' import type { ThunkDispatch } from 'redux-thunk' import type { NormalizedPipette } from '@opentrons/step-generation' @@ -151,12 +150,11 @@ const validationSchema: any = Yup.object().shape({ }) export function CreateNewProtocolWizard(): JSX.Element | null { - const { t } = useTranslation(['modal', 'alert']) - const hasUnsavedChanges = useSelector(loadFileSelectors.getHasUnsavedChanges) + const navigate = useNavigate() + const showWizard = useSelector(getNewProtocolModal) const customLabware = useSelector( labwareDefSelectors.getCustomLabwareDefsByURI ) - const navigate = useNavigate() const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const [wizardSteps, setWizardSteps] = React.useState( WIZARD_STEPS @@ -164,6 +162,12 @@ export function CreateNewProtocolWizard(): JSX.Element | null { const dispatch = useDispatch>() + React.useEffect(() => { + if (!showWizard) { + navigate('/overview') + } + }, [showWizard]) + const createProtocolFile = (values: WizardFormState): void => { navigate('/overview') @@ -215,143 +219,141 @@ export function CreateNewProtocolWizard(): JSX.Element | null { } const newProtocolFields = values.fields - if ( - !hasUnsavedChanges || - window.confirm(t('alert:confirm_create_new') as string) - ) { - dispatch(fileActions.createNewProtocol(newProtocolFields)) - const pipettesById: Record = pipettes.reduce( - (acc, pipette) => ({ ...acc, [uuid()]: pipette }), - {} + dispatch(fileActions.createNewProtocol(newProtocolFields)) + const pipettesById: Record = pipettes.reduce( + (acc, pipette) => ({ ...acc, [uuid()]: pipette }), + {} + ) + // create custom labware + mapValues(customLabware, labwareDef => + dispatch( + labwareDefActions.createCustomLabwareDefAction({ + def: labwareDef, + }) ) - // create custom labware - mapValues(customLabware, labwareDef => - dispatch( - labwareDefActions.createCustomLabwareDefAction({ - def: labwareDef, + ) + // create new pipette entities + dispatch( + stepFormActions.createPipettes( + mapValues( + pipettesById, + (p: PipetteOnDeck, id: string): NormalizedPipette => ({ + // @ts-expect-error(sa, 2021-6-22): id will always get overwritten + id, + ...omit(p, 'mount'), }) ) ) - // create new pipette entities - dispatch( - stepFormActions.createPipettes( - mapValues( + ) + // update pipette locations in initial deck setup step + dispatch( + steplistActions.changeSavedStepForm({ + stepId: INITIAL_DECK_SETUP_STEP_ID, + update: { + pipetteLocationUpdate: mapValues( pipettesById, - (p: PipetteOnDeck, id: string): NormalizedPipette => ({ - // @ts-expect-error(sa, 2021-6-22): id will always get overwritten - id, - ...omit(p, 'mount'), - }) - ) - ) - ) - // update pipette locations in initial deck setup step + (p: typeof pipettesById[keyof typeof pipettesById]) => p.mount + ), + }, + }) + ) + + // add trash + if (values.additionalEquipment.includes('trashBin')) { + // defaulting trash to appropriate locations dispatch( - steplistActions.changeSavedStepForm({ - stepId: INITIAL_DECK_SETUP_STEP_ID, - update: { - pipetteLocationUpdate: mapValues( - pipettesById, - (p: typeof pipettesById[keyof typeof pipettesById]) => p.mount - ), - }, - }) + createDeckFixture( + 'trashBin', + values.fields.robotType === FLEX_ROBOT_TYPE + ? getTrashSlot(values) + : 'cutout12' + ) ) + } - // add trash - if (values.additionalEquipment.includes('trashBin')) { - // defaulting trash to appropriate locations - dispatch( - createDeckFixture( - 'trashBin', - values.fields.robotType === FLEX_ROBOT_TYPE - ? // TODO(ja, 8/9/24): add logic for which trash location for flex to default to - 'cutoutA3' - : 'cutout12' - ) + // add waste chute + if (values.additionalEquipment.includes('wasteChute')) { + dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_CUTOUT)) + } + // add staging areas + const stagingAreas = values.additionalEquipment.filter( + equipment => equipment === 'stagingArea' + ) + if (stagingAreas.length > 0) { + stagingAreas.forEach((_, index) => { + return dispatch( + createDeckFixture('stagingArea', STAGING_AREA_CUTOUTS[index]) ) - } + }) + } - // add waste chute - if (values.additionalEquipment.includes('wasteChute')) { - dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_CUTOUT)) + // create modules + // sort so modules with slot are created first + // then modules without a slot are generated in remaining available slots + modules.sort((a, b) => { + if (a.slot == null && b.slot != null) { + return 1 } - // add staging areas - const stagingAreas = values.additionalEquipment.filter(equipment => - equipment.includes('stagingArea') - ) - if (stagingAreas.length > 0) { - stagingAreas.forEach(stagingArea => { - const [, location] = stagingArea.split('_') - dispatch(createDeckFixture('stagingArea', location)) - }) + if (b.slot == null && a.slot != null) { + return -1 } + return 0 + }) - // create modules - // sort so modules with slot are created first - // then modules without a slot are generated in remaining available slots - modules.sort((a, b) => { - if (a.slot == null && b.slot != null) { - return 1 - } - if (b.slot == null && a.slot != null) { - return -1 - } - return 0 - }) + modules.forEach(moduleArgs => { + return moduleArgs.slot != null + ? dispatch(stepFormActions.createModule(moduleArgs)) + : dispatch( + createModuleWithNoSlot({ + model: moduleArgs.model, + type: moduleArgs.type, + isMagneticBlock: moduleArgs.type === MAGNETIC_BLOCK_TYPE, + }) + ) + }) - modules.forEach(moduleArgs => { - return moduleArgs.slot != null - ? dispatch(stepFormActions.createModule(moduleArgs)) - : dispatch( - createModuleWithNoSlot({ - model: moduleArgs.model, - type: moduleArgs.type, - isMagneticBlock: moduleArgs.type === MAGNETIC_BLOCK_TYPE, - }) - ) - }) + // add gripper + if (values.additionalEquipment.includes('gripper')) { + dispatch(toggleIsGripperRequired()) + } - // add gripper - if (values.additionalEquipment.includes('gripper')) { - dispatch(toggleIsGripperRequired()) - } - // auto-generate tipracks for pipettes - const newTiprackModels: string[] = uniq( - pipettes.flatMap(pipette => pipette.tiprackDefURI) - ) - const hasMagneticBlock = modules.some( - module => module.type === MAGNETIC_BLOCK_TYPE - ) - const FLEX_MIDDLE_SLOTS = hasMagneticBlock ? [] : ['C2', 'B2', 'A2'] - const hasOt2TC = modules.find( - module => module.type === THERMOCYCLER_MODULE_TYPE - ) - const heaterShakerSlot = modules.find( - module => module.type === HEATERSHAKER_MODULE_TYPE - )?.slot - const OT2_MIDDLE_SLOTS = hasOt2TC ? ['2', '5'] : ['2', '5', '8', '11'] - const modifiedOt2Slots = OT2_MIDDLE_SLOTS.filter(slot => - heaterShakerSlot != null - ? !getAreSlotsAdjacent(heaterShakerSlot, slot) - : slot + // auto-generate assigned tipracks for pipettes + const newTiprackModels: string[] = uniq( + pipettes.flatMap(pipette => pipette.tiprackDefURI) + ) + const hasMagneticBlock = modules.some( + module => module.type === MAGNETIC_BLOCK_TYPE + ) + const FLEX_MIDDLE_SLOTS = hasMagneticBlock ? [] : ['C2', 'B2', 'A2'] + const hasOt2TC = modules.find( + module => module.type === THERMOCYCLER_MODULE_TYPE + ) + const heaterShakerSlot = modules.find( + module => module.type === HEATERSHAKER_MODULE_TYPE + )?.slot + const OT2_MIDDLE_SLOTS = hasOt2TC ? ['2', '5'] : ['2', '5', '8', '11'] + const modifiedOt2Slots = OT2_MIDDLE_SLOTS.filter(slot => + heaterShakerSlot != null + ? !getAreSlotsAdjacent(heaterShakerSlot, slot) + : slot + ) + newTiprackModels.forEach((tiprackDefURI, index) => { + dispatch( + labwareIngredActions.createContainer({ + slot: + values.fields.robotType === FLEX_ROBOT_TYPE + ? FLEX_MIDDLE_SLOTS[index] + : modifiedOt2Slots[index], + labwareDefURI: tiprackDefURI, + adapterUnderLabwareDefURI: + values.pipettesByMount.left.pipetteName === 'p1000_96' + ? adapter96ChannelDefUri + : undefined, + }) ) - newTiprackModels.forEach((tiprackDefURI, index) => { - dispatch( - labwareIngredActions.createContainer({ - slot: - values.fields.robotType === FLEX_ROBOT_TYPE - ? FLEX_MIDDLE_SLOTS[index] - : modifiedOt2Slots[index], - labwareDefURI: tiprackDefURI, - adapterUnderLabwareDefURI: - values.pipettesByMount.left.pipetteName === 'p1000_96' - ? adapter96ChannelDefUri - : undefined, - }) - ) - }) - } + }) + + dispatch(labwareIngredActions.generateNewProtocol({ isNewProtocol: true })) } const currentWizardStep = wizardSteps[currentStepIndex] @@ -366,8 +368,8 @@ export function CreateNewProtocolWizard(): JSX.Element | null { } } - return ( - + return showWizard ? ( + - ) + ) : null } interface CreateFileFormProps { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/types.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/types.ts index afc4696a993..f27ec05ba12 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/types.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/types.ts @@ -7,10 +7,7 @@ export type AdditionalEquipment = | 'gripper' | 'wasteChute' | 'trashBin' - | 'stagingArea_cutoutA3' - | 'stagingArea_cutoutB3' - | 'stagingArea_cutoutC3' - | 'stagingArea_cutoutD3' + | 'stagingArea' export interface WizardFormState { fields: NewProtocolFields diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx index a23144b2015..0d1d959da36 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx @@ -1,27 +1,47 @@ import * as React from 'react' import { MAGNETIC_BLOCK_TYPE, + STAGING_AREA_CUTOUTS, THERMOCYCLER_MODULE_TYPE, + getLabwareDefURI, + getLabwareDisplayName, + getPipetteSpecsV2, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import wasteChuteImage from '../../assets/images/waste_chute.png' import trashBinImage from '../../assets/images/flex_trash_bin.png' import stagingAreaImage from '../../assets/images/staging_area.png' - +import type { + CutoutId, + LabwareDefByDefURI, + LabwareDefinition2, + PipetteName, +} from '@opentrons/shared-data' +import type { DropdownOption } from '@opentrons/components' import type { AdditionalEquipment, WizardFormState } from './types' const TOTAL_MODULE_SLOTS = 8 const MIDDLE_SLOT_NUM = 4 +export const getNumOptions = (length: number): DropdownOption[] => { + return Array.from({ length }, (_, i) => ({ + name: `${i + 1}`, + value: `${i + 1}`, + })) +} + export const getNumSlotsAvailable = ( modules: WizardFormState['modules'], - additionalEquipment: WizardFormState['additionalEquipment'], - // special-casing the wasteChute available slots when there is a staging area in slot 3 - isWasteChute?: boolean + additionalEquipment: WizardFormState['additionalEquipment'] ): number => { const additionalEquipmentLength = additionalEquipment.length const hasTC = Object.values(modules || {}).some( module => module.type === THERMOCYCLER_MODULE_TYPE ) + const numStagingAreas = additionalEquipment.filter(ae => ae === 'stagingArea') + ?.length + const hasWasteChute = additionalEquipment.some(ae => ae === 'wasteChute') + const magneticBlocks = Object.values(modules || {}).filter( module => module.type === MAGNETIC_BLOCK_TYPE ) @@ -37,24 +57,15 @@ export const getNumSlotsAvailable = ( filteredModuleLength = filteredModuleLength - numBlocks } - const hasWasteChute = additionalEquipment.some(equipment => - equipment.includes('wasteChute') - ) - const isStagingAreaInD3 = additionalEquipment - .filter(equipment => equipment.includes('stagingArea')) - .find(stagingArea => stagingArea.split('_')[1] === 'cutoutD3') const hasGripper = additionalEquipment.some(equipment => equipment.includes('gripper') ) let filteredAdditionalEquipmentLength = additionalEquipmentLength - if (hasWasteChute && isStagingAreaInD3) { - filteredAdditionalEquipmentLength = filteredAdditionalEquipmentLength - 1 - } - if (isWasteChute && isStagingAreaInD3) { + if (hasGripper) { filteredAdditionalEquipmentLength = filteredAdditionalEquipmentLength - 1 } - if (hasGripper) { + if (numStagingAreas === MIDDLE_SLOT_NUM && hasWasteChute) { filteredAdditionalEquipmentLength = filteredAdditionalEquipmentLength - 1 } return ( @@ -105,3 +116,124 @@ export function AdditionalEquipmentDiagram(props: EquipmentProps): JSX.Element { } } } + +interface TiprackOptionsProps { + allLabware: LabwareDefByDefURI + allowAllTipracks: boolean + selectedPipetteName?: string | null +} +// returns a hashmap of LabwareDefUri : displayName +export function getTiprackOptions( + props: TiprackOptionsProps +): Record { + const { allLabware, allowAllTipracks, selectedPipetteName } = props + + if (!allLabware) return {} + + const pipetteSpecs = selectedPipetteName + ? getPipetteSpecsV2(selectedPipetteName as PipetteName) + : null + + const defaultTipracks = pipetteSpecs?.liquids.default.defaultTipracks ?? [] + const displayCategory = pipetteSpecs?.displayCategory ?? '' + const isFlexPipette = + displayCategory === 'FLEX' || selectedPipetteName === 'p1000_96' + + const tiprackOptionsMap = Object.values(allLabware) + .filter(def => def.metadata.displayCategory === 'tipRack') + .filter(def => { + if (allowAllTipracks) { + return isFlexPipette + ? def.metadata.displayName.includes('Flex') || + def.namespace === 'custom_beta' + : !def.metadata.displayName.includes('Flex') || + def.namespace === 'custom_beta' + } + return ( + defaultTipracks.includes(getLabwareDefURI(def)) || + def.namespace === 'custom_beta' + ) + }) + .reduce((acc: Record, def: LabwareDefinition2) => { + const displayName = getLabwareDisplayName(def) + const name = + def.parameters.loadName.includes('flex') && isFlexPipette + ? displayName.split('Opentrons Flex')[1] + : displayName + acc[getLabwareDefURI(def)] = name + return acc + }, {}) + + return tiprackOptionsMap +} + +export const MOVABLE_TRASH_CUTOUTS = [ + { + value: 'cutoutA3', + slot: 'A3', + }, + { + value: 'cutoutA1', + slot: 'A1', + }, + { + value: 'cutoutB1', + slot: 'B1', + }, + { + value: 'cutoutB3', + slot: 'B3', + }, + { + value: 'cutoutC1', + slot: 'C1', + }, + { + value: 'cutoutC3', + slot: 'C3', + }, + { + value: 'cutoutD1', + slot: 'D1', + }, + { + value: 'cutoutD3', + slot: 'D3', + }, +] + +export const getTrashSlot = (values: WizardFormState): string => { + const { additionalEquipment, modules } = values + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] + const stagingAreas = additionalEquipment.filter(equipment => + equipment.includes('stagingArea') + ) + + const cutouts = stagingAreas.map((_, index) => STAGING_AREA_CUTOUTS[index]) + const hasWasteChute = additionalEquipment.find(equipment => + equipment.includes('wasteChute') + ) + const wasteChuteSlot = Boolean(hasWasteChute) + ? [WASTE_CHUTE_CUTOUT as string] + : [] + const unoccupiedSlot = MOVABLE_TRASH_CUTOUTS.find( + cutout => + !cutouts.includes(cutout.value as CutoutId) && + !moduleSlots.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value) + ) + if (unoccupiedSlot == null) { + console.error( + 'Expected to find an unoccupied slot for the trash bin but could not' + ) + return '' + } + return unoccupiedSlot?.value +} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/ControlSelect.tsx b/protocol-designer/src/pages/Designer/DeckSetup/ControlSelect.tsx deleted file mode 100644 index f8a99e2fab3..00000000000 --- a/protocol-designer/src/pages/Designer/DeckSetup/ControlSelect.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from 'react' -import { useTranslation } from 'react-i18next' -import cx from 'classnames' -import { css } from 'styled-components' -import { useSelector } from 'react-redux' -import { - Flex, - LegacyStyledText, - RobotCoordsForeignDiv, -} from '@opentrons/components' -import { START_TERMINAL_ITEM_ID } from '../../../steplist' -import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' - -import type { CoordinateTuple, Dimensions } from '@opentrons/shared-data' -import type { TerminalItemId } from '../../../steplist' - -import styles from './DeckSetup.module.css' - -interface ControlSelectProps { - slotPosition: CoordinateTuple | null - slotBoundingBox: Dimensions - slotId: string - addEquipment: (slotId: string) => void - hover: string | null - setHover: React.Dispatch> - slotTopLayerId: string // can be AddressableAreaName, moduleId, labwareId - selectedTerminalItemId?: TerminalItemId | null -} - -export const ControlSelect = ( - props: ControlSelectProps -): JSX.Element | null => { - const { - slotBoundingBox, - slotPosition, - slotId, - selectedTerminalItemId, - addEquipment, - hover, - setHover, - slotTopLayerId, - } = props - const { t } = useTranslation('starting_deck_state') - const activeDeckSetup = useSelector(getDeckSetupForActiveItem) - const moduleId = Object.keys(activeDeckSetup.modules).find( - moduleId => slotTopLayerId === moduleId - ) - - if (selectedTerminalItemId !== START_TERMINAL_ITEM_ID || slotPosition == null) - return null - - return ( - { - setHover(slotId) - }, - onMouseLeave: () => { - setHover(null) - }, - onClick: () => { - addEquipment(slotId) - }, - }} - > - - { - addEquipment(slotId) - }} - > - - {moduleId != null ? t('add_labware') : t('edit')} - - - - - ) -} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckItemHover.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckItemHover.tsx new file mode 100644 index 00000000000..f859ba452cd --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckItemHover.tsx @@ -0,0 +1,125 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { css } from 'styled-components' +import { + ALIGN_CENTER, + BORDERS, + COLORS, + DISPLAY_FLEX, + Flex, + JUSTIFY_CENTER, + Link, + POSITION_ABSOLUTE, + PRODUCT, + RobotCoordsForeignDiv, + StyledText, +} from '@opentrons/components' +import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' +import { START_TERMINAL_ITEM_ID } from '../../../steplist' + +import type { + CoordinateTuple, + DeckSlotId, + Dimensions, +} from '@opentrons/shared-data' +import type { TerminalItemId } from '../../../steplist' + +interface DeckItemHoverProps { + hover: string | null + setHover: React.Dispatch> + slotBoundingBox: Dimensions + // can be slotId or labwareId (for off-deck labware) + itemId: string + slotPosition: CoordinateTuple | null + setShowMenuListForId: React.Dispatch> + menuListId: DeckSlotId | null + isSelected?: boolean + selectedTerminalItemId?: TerminalItemId | null +} + +export function DeckItemHover(props: DeckItemHoverProps): JSX.Element | null { + const { + hover, + selectedTerminalItemId, + setHover, + slotBoundingBox, + itemId, + setShowMenuListForId, + menuListId, + slotPosition, + isSelected = false, + } = props + const { t } = useTranslation('starting_deck_state') + const deckSetup = useSelector(getDeckSetupForActiveItem) + const offDeckLabware = Object.values(deckSetup.labware).find( + lw => lw.id === itemId + ) + if ( + selectedTerminalItemId !== START_TERMINAL_ITEM_ID || + slotPosition === null || + isSelected + ) + return null + + const hoverOpacity = + (hover != null && hover === itemId) || menuListId === itemId ? '1' : '0' + + return ( + { + setHover(itemId) + }, + onMouseLeave: () => { + setHover(null) + }, + onClick: () => { + setShowMenuListForId(itemId) + }, + }} + > + + { + setShowMenuListForId(itemId) + }} + > + + {offDeckLabware?.slot === 'offDeck' + ? t('edit_labware') + : t('edit_slot')} + + + + + ) +} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetup.module.css b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetup.module.css deleted file mode 100644 index b4b88d3a8f8..00000000000 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetup.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.slot_overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1; - padding: 0.5rem; - background-color: color-mod(var(--c-black) alpha(0.75)); - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: flex-start; - color: white; - font-size: var(--fs-body-1); - border-radius: 0.5rem; -} - -.appear_on_mouseover { - opacity: 0; - - &:hover { - opacity: 1; - } -} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx index 8daf06defd6..669bbbe0329 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx @@ -1,12 +1,13 @@ import * as React from 'react' -import { useSelector } from 'react-redux' -import { useNavigate } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' import { + ALIGN_CENTER, + BORDERS, COLORS, DeckFromLayers, Flex, FlexTrash, - PrimaryButton, + JUSTIFY_CENTER, RobotCoordinateSpaceWithRef, SingleSlotFixture, SlotLabels, @@ -15,8 +16,8 @@ import { WasteChuteStagingAreaFixture, } from '@opentrons/components' import { - FLEX_ROBOT_TYPE, getDeckDefFromRobotType, + getPositionFromSlotId, isAddressableAreaStandardSlot, OT2_ROBOT_TYPE, STAGING_AREA_CUTOUTS, @@ -28,22 +29,28 @@ import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locati import { getDisableModuleRestrictions } from '../../../feature-flags/selectors' import { getRobotType } from '../../../file-data/selectors' import { getHasGen1MultiChannelPipette } from '../../../step-forms' -import { SlotDetailsContainer } from './SlotDetailsContainer' +import { SlotDetailsContainer } from '../../../organisms' +import { selectZoomedIntoSlot } from '../../../labware-ingred/actions' +import { selectors } from '../../../labware-ingred/selectors' import { DeckSetupDetails } from './DeckSetupDetails' -import { getCutoutIdForAddressableArea } from './utils' +import { + animateZoom, + getCutoutIdForAddressableArea, + zoomInOnCoordinate, +} from './utils' import { DeckSetupTools } from './DeckSetupTools' import type { StagingAreaLocation, TrashCutoutId } from '@opentrons/components' -import type { AddressableAreaName, CutoutId } from '@opentrons/shared-data' +import type { + AddressableAreaName, + CutoutId, + ModuleModel, +} from '@opentrons/shared-data' import type { AdditionalEquipmentEntity, DeckSlot, } from '@opentrons/step-generation' - -interface OpenSlot { - cutoutId: CutoutId - slot: DeckSlot -} +import type { Fixture } from './constants' const WASTE_CHUTE_SPACE = 30 const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ @@ -56,34 +63,87 @@ const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ 'screwHoles', 'fixedTrash', ] - -const lightFill = COLORS.grey35 -const darkFill = COLORS.grey60 +export const lightFill = COLORS.grey35 export function DeckSetupContainer(): JSX.Element { const selectedTerminalItemId = useSelector(getSelectedTerminalItemId) const activeDeckSetup = useSelector(getDeckSetupForActiveItem) + const dispatch = useDispatch() + const zoomIn = useSelector(selectors.getZoomedInSlot) const _disableCollisionWarnings = useSelector(getDisableModuleRestrictions) const robotType = useSelector(getRobotType) - const deckDef = React.useMemo(() => getDeckDefFromRobotType(robotType), []) - const [hover, setHover] = React.useState(null) - const [zoomIn, setZoomInOnSlot] = React.useState(null) + const deckDef = React.useMemo(() => getDeckDefFromRobotType(robotType), [ + robotType, + ]) + const [hoverSlot, setHoverSlot] = React.useState(null) const trash = Object.values(activeDeckSetup.additionalEquipmentOnDeck).find( ae => ae.name === 'trashBin' ) + const wasteChuteFixtures = Object.values( + activeDeckSetup.additionalEquipmentOnDeck + ).filter( + aE => + WASTE_CHUTE_CUTOUT.includes(aE.location as CutoutId) && + aE.name === 'wasteChute' + ) + const wasteChuteStagingAreaFixtures = Object.values( + activeDeckSetup.additionalEquipmentOnDeck + ).filter( + aE => + STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) && + aE.name === 'stagingArea' && + aE.location === WASTE_CHUTE_CUTOUT && + wasteChuteFixtures.length > 0 + ) + const hasWasteChute = + wasteChuteFixtures.length > 0 || wasteChuteStagingAreaFixtures.length > 0 - const navigate = useNavigate() + const initialViewBox = `${deckDef.cornerOffsetFromOrigin[0]} ${ + hasWasteChute + ? deckDef.cornerOffsetFromOrigin[1] - WASTE_CHUTE_SPACE + : deckDef.cornerOffsetFromOrigin[1] + } ${deckDef.dimensions[0]} ${deckDef.dimensions[1]}` + + const [viewBox, setViewBox] = React.useState(initialViewBox) + const [hoveredLabware, setHoveredLabware] = React.useState( + null + ) + const [hoveredModule, setHoveredModule] = React.useState( + null + ) + const [hoveredFixture, setHoveredFixture] = React.useState( + null + ) const addEquipment = (slotId: string): void => { const cutoutId = getCutoutIdForAddressableArea( slotId as AddressableAreaName, deckDef.cutoutFixtures - ) ?? 'cutoutD1' - setZoomInOnSlot({ cutoutId, slot: slotId }) - } + ) ?? null + if (cutoutId == null) { + console.error('expected to find a cutoutId but could not') + } + dispatch(selectZoomedIntoSlot({ slot: slotId, cutout: cutoutId })) - const trashSlot = trash?.location + const zoomInSlotPosition = getPositionFromSlotId(slotId ?? '', deckDef) + if (zoomInSlotPosition != null) { + const zoomedInViewBox = zoomInOnCoordinate({ + x: zoomInSlotPosition[0], + y: zoomInSlotPosition[1], + + deckDef, + }) + // TODO(ja, 9/3/24): re-examine this usage. It is causing + // a handful of rerendering of the DeckSetupTools which may + // cause optimization issues?? + animateZoom({ + targetViewBox: zoomedInViewBox, + viewBox, + setViewBox, + }) + } + } const _hasGen1MultichannelPipette = React.useMemo( () => getHasGen1MultiChannelPipette(activeDeckSetup.pipettes), @@ -98,13 +158,6 @@ export function DeckSetupContainer(): JSX.Element { cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, ] - const wasteChuteFixtures = Object.values( - activeDeckSetup.additionalEquipmentOnDeck - ).filter( - aE => - WASTE_CHUTE_CUTOUT.includes(aE.location as CutoutId) && - aE.name === 'wasteChute' - ) const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values( activeDeckSetup.additionalEquipmentOnDeck ).filter( @@ -113,54 +166,29 @@ export function DeckSetupContainer(): JSX.Element { aE.name === 'stagingArea' ) - const wasteChuteStagingAreaFixtures = Object.values( - activeDeckSetup.additionalEquipmentOnDeck - ).filter( - aE => - STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) && - aE.name === 'stagingArea' && - aE.location === WASTE_CHUTE_CUTOUT && - wasteChuteFixtures.length > 0 - ) - - const hasWasteChute = - wasteChuteFixtures.length > 0 || wasteChuteStagingAreaFixtures.length > 0 - const filteredAddressableAreas = deckDef.locations.addressableAreas.filter( aa => isAddressableAreaStandardSlot(aa.id, deckDef) ) + return ( <> - { - navigate('/overview') - }} + - exit - - {zoomIn != null ? ( - // TODO(ja, 8/6/24): still need to develop the zoomed in slot - { - setZoomInOnSlot(null) - }} - cutoutId={zoomIn.cutoutId} - slot={zoomIn.slot} - /> - ) : ( {() => ( <> @@ -186,18 +214,26 @@ export function DeckSetupContainer(): JSX.Element { /> ) : null })} - {stagingAreaFixtures.map(fixture => ( - - ))} + {stagingAreaFixtures.map(fixture => { + if ( + zoomIn.cutout == null || + zoomIn.cutout !== fixture.location + ) { + return ( + + ) + } + })} {trash != null ? trashBinFixtures.map(({ cutoutId }) => - cutoutId != null ? ( + cutoutId != null && + (zoomIn.cutout == null || + zoomIn.cutout !== cutoutId) ? ( ( - - ))} - {wasteChuteStagingAreaFixtures.map(fixture => ( - - ))} + {wasteChuteFixtures.map(fixture => { + if ( + zoomIn.cutout == null || + zoomIn.cutout !== fixture.location + ) { + return ( + + ) + } + })} + {wasteChuteStagingAreaFixtures.map(fixture => { + if ( + zoomIn.cutout == null || + zoomIn.cutout !== fixture.location + ) { + return ( + + ) + } + })} )} 0} /> - {hover != null ? ( - + {hoverSlot != null ? ( + ) : null} )} - )} +
+ {zoomIn.slot != null && zoomIn.cutout != null ? ( + { + dispatch(selectZoomedIntoSlot({ slot: null, cutout: null })) + animateZoom({ + targetViewBox: initialViewBox, + viewBox, + setViewBox, + }) + }} + setHoveredLabware={setHoveredLabware} + /> + ) : null} ) } diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx index ddf4e25897e..d1e6b435177 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx @@ -1,35 +1,42 @@ import * as React from 'react' -import compact from 'lodash/compact' import values from 'lodash/values' +import { useDispatch, useSelector } from 'react-redux' import { Module } from '@opentrons/components' import { MODULES_WITH_COLLISION_ISSUES } from '@opentrons/step-generation' import { getAddressableAreaFromSlotId, + getAreSlotsVerticallyAdjacent, getLabwareHasQuirk, getModuleDef2, getPositionFromSlotId, inferModuleOrientationFromSlot, inferModuleOrientationFromXCoordinate, isAddressableAreaStandardSlot, - SPAN7_8_10_11_SLOT, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { - getSlotIdsBlockedBySpanning, - getSlotIsEmpty, -} from '../../../step-forms' +import { getSlotIdsBlockedBySpanningForThermocycler } from '../../../step-forms' import { LabwareOnDeck } from '../../../components/DeckSetup/LabwareOnDeck' +import { selectors } from '../../../labware-ingred/selectors' import { SlotWarning } from '../../../components/DeckSetup/SlotWarning' import { getStagingAreaAddressableAreas } from '../../../utils' -import { ControlSelect } from './ControlSelect' +import { editSlotInfo } from '../../../labware-ingred/actions' +import { getRobotType } from '../../../file-data/selectors' +import { getSlotInformation } from '../utils' +import { DeckItemHover } from './DeckItemHover' +import { SlotOverflowMenu } from './SlotOverflowMenu' +import { HoveredItems } from './HoveredItems' +import { SelectedHoveredItems } from './SelectedHoveredItems' import type { ModuleTemporalProperties } from '@opentrons/step-generation' import type { + AddressableArea, AddressableAreaName, CutoutId, DeckDefinition, + DeckSlotId, Dimensions, + ModuleModel, } from '@opentrons/shared-data' import type { InitialDeckSetup, @@ -37,32 +44,77 @@ import type { ModuleOnDeck, } from '../../../step-forms' import type { TerminalItemId } from '../../../steplist' +import type { Fixture } from './constants' interface DeckSetupDetailsProps { activeDeckSetup: InitialDeckSetup - showGen1MultichannelCollisionWarnings: boolean - deckDef: DeckDefinition - stagingAreaCutoutIds: CutoutId[] - trashSlot: string | null addEquipment: (slotId: string) => void + deckDef: DeckDefinition hover: string | null + hoveredFixture: Fixture | null + hoveredLabware: string | null + hoveredModule: ModuleModel | null setHover: React.Dispatch> + showGen1MultichannelCollisionWarnings: boolean + stagingAreaCutoutIds: CutoutId[] selectedTerminalItemId?: TerminalItemId | null + selectedZoomInSlot?: DeckSlotId } export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => { const { activeDeckSetup, - showGen1MultichannelCollisionWarnings, - deckDef, - trashSlot, addEquipment, - stagingAreaCutoutIds, - selectedTerminalItemId, + deckDef, hover, + hoveredFixture, + hoveredLabware, + hoveredModule, + selectedTerminalItemId, + selectedZoomInSlot, setHover, + showGen1MultichannelCollisionWarnings, + stagingAreaCutoutIds, } = props - const slotIdsBlockedBySpanning = getSlotIdsBlockedBySpanning(activeDeckSetup) + const robotType = useSelector(getRobotType) + const slotIdsBlockedBySpanning = getSlotIdsBlockedBySpanningForThermocycler( + activeDeckSetup, + robotType + ) + const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) + const { selectedSlot } = selectedSlotInfo + const [menuListId, setShowMenuListForId] = React.useState( + null + ) + const dispatch = useDispatch() + + const { + createdLabwareForSlot, + createdNestedLabwareForSlot, + createdModuleForSlot, + preSelectedFixture, + slotPosition, + } = getSlotInformation({ + deckSetup: activeDeckSetup, + slot: selectedZoomInSlot ?? '', + deckDef, + }) + // initiate the slot's info + React.useEffect(() => { + dispatch( + editSlotInfo({ + createdNestedLabwareForSlot, + createdLabwareForSlot, + createdModuleForSlot, + preSelectedFixture, + }) + ) + }, [ + createdLabwareForSlot, + createdNestedLabwareForSlot, + createdModuleForSlot, + preSelectedFixture, + ]) const allLabware: LabwareOnDeckType[] = Object.keys( activeDeckSetup.labware @@ -74,33 +126,17 @@ export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => { }, []) const allModules: ModuleOnDeck[] = values(activeDeckSetup.modules) + const menuListSlotPosition = getPositionFromSlotId(menuListId ?? '', deckDef) - // NOTE: naively hard-coded to show warning north of slots 1 or 3 when occupied by any module const multichannelWarningSlotIds: AddressableAreaName[] = showGen1MultichannelCollisionWarnings - ? compact([ - allModules.some( - moduleOnDeck => - moduleOnDeck.slot === '1' && - MODULES_WITH_COLLISION_ISSUES.includes(moduleOnDeck.model) - ) - ? deckDef.locations.addressableAreas.find(s => s.id === '4')?.id - : null, - allModules.some( - moduleOnDeck => - moduleOnDeck.slot === '3' && - MODULES_WITH_COLLISION_ISSUES.includes(moduleOnDeck.model) - ) - ? deckDef.locations.addressableAreas.find(s => s.id === '6')?.id - : null, - ]) + ? getSlotsWithCollisions(deckDef, allModules) : [] return ( <> {/* all modules */} {allModules.map(moduleOnDeck => { - const slotId = - moduleOnDeck.slot === SPAN7_8_10_11_SLOT ? '7' : moduleOnDeck.slot + const slotId = moduleOnDeck.slot const slotPosition = getPositionFromSlotId(slotId, deckDef) if (slotPosition == null) { @@ -108,7 +144,6 @@ export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => { return null } const moduleDef = getModuleDef2(moduleOnDeck.model) - const getModuleInnerProps = ( moduleState: ModuleTemporalProperties['moduleState'] ): React.ComponentProps['innerProps'] => { @@ -139,6 +174,7 @@ export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => { } } } + const labwareLoadedOnModule = allLabware.find( lw => lw.slot === moduleOnDeck.id ) @@ -152,10 +188,10 @@ export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => { yDimension: labwareLoadedOnModule?.def.dimensions.yDimension ?? 0, zDimension: labwareLoadedOnModule?.def.dimensions.zDimension ?? 0, } - return ( + return moduleOnDeck.slot !== selectedSlot.slot ? ( { y={0} labwareOnDeck={labwareLoadedOnModule} /> - ) : null} {labwareLoadedOnModule == null ? ( - ) : null} - ) + ) : null })} {/* on-deck warnings for OT-2 and GEN1 8-channels only */} @@ -232,31 +270,29 @@ export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => { stagingAreaAddressableAreas.includes(addressableArea.id) return ( addressableAreas && - !slotIdsBlockedBySpanning.includes(addressableArea.id) && - getSlotIsEmpty(activeDeckSetup, addressableArea.id) && - addressableArea.id !== trashSlot + !slotIdsBlockedBySpanning.includes(addressableArea.id) ) }) .map(addressableArea => { return ( - ) })} - {/* all labware on deck NOT those in modules */} {allLabware.map(labware => { if ( @@ -275,25 +311,26 @@ export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => { console.warn(`no slot ${labware.slot} for labware ${labware.id}!`) return null } - return ( + return labware.slot !== selectedSlot.slot ? ( - - ) + ) : null })} {/* all nested labwares on deck */} @@ -329,10 +366,13 @@ export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => { yDimension: labware.def.dimensions.yDimension, zDimension: labware.def.dimensions.zDimension, } + const moduleParent = allModules.find( + module => module.id === slotForOnTheDeck + ) const slotOnDeck = - slotForOnTheDeck != null - ? allModules.find(module => module.id === slotForOnTheDeck)?.slot - : null + moduleParent == null + ? slotForOnTheDeck + : allModules.find(module => module.id === slotForOnTheDeck)?.slot return ( { y={slotPosition[1]} labwareOnDeck={labware} /> - ) })} + + {/* selected hardware + labware */} + + + {/* hovered hardware + labware */} + + + {/* slot overflow menu */} + {menuListSlotPosition != null && menuListId != null ? ( + { + setShowMenuListForId(null) + }} + /> + ) : null} ) } + +const getSlotsWithCollisions = ( + deckDef: DeckDefinition, + allModules: ModuleOnDeck[] +): AddressableAreaName[] => { + return deckDef.locations.addressableAreas.reduce( + (acc: AddressableAreaName[], aa: AddressableArea) => { + const modulesWithCollisionsOnDeck = allModules.filter(module => + MODULES_WITH_COLLISION_ISSUES.includes(module.model) + ) + if (modulesWithCollisionsOnDeck.length === 0) { + return acc + } + + const hasCollision = modulesWithCollisionsOnDeck.some(module => + getAreSlotsVerticallyAdjacent(module.slot, aa.id) + ) + if (hasCollision) { + return [...acc, aa.id] + } + return acc + }, + [] + ) +} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 1b16ce316ca..f8217fb7506 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -2,8 +2,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { + ALIGN_CENTER, DIRECTION_COLUMN, + DeckInfoLabel, Flex, + ModuleIcon, RadioButton, SPACING, StyledText, @@ -11,8 +14,7 @@ import { Toolbox, } from '@opentrons/components' import { - MAGNETIC_BLOCK_V1, - MODULE_MODELS, + FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE, getModuleDisplayName, getModuleType, @@ -28,88 +30,95 @@ import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locati import { createContainer, deleteContainer, + selectFixture, + selectLabware, + selectModule, + editSlotInfo, + selectZoomedIntoSlot, + selectNestedLabware, } from '../../../labware-ingred/actions' import { getEnableAbsorbanceReader, getEnableMoam, } from '../../../feature-flags/selectors' +import { selectors } from '../../../labware-ingred/selectors' +import { useKitchen } from '../../../organisms/Kitchen/hooks' import { createContainerAboveModule } from '../../../step-forms/actions/thunks' -import { - FIXTURES, - MAX_MAGNETIC_BLOCKS, - MAX_MOAM_MODULES, - MOAM_MODELS, - MOAM_MODELS_WITH_FF, -} from './constants' -import { getModuleModelsBySlot } from './utils' +import { FIXTURES, MOAM_MODELS, MOAM_MODELS_WITH_FF } from './constants' +import { getSlotInformation } from '../utils' +import { getModuleModelsBySlot, getDeckErrors } from './utils' import { LabwareTools } from './LabwareTools' -import type { CutoutId, DeckSlotId, ModuleModel } from '@opentrons/shared-data' -import type { DeckFixture } from '../../../step-forms/actions/additionalItems' +import type { ModuleModel } from '@opentrons/shared-data' import type { ThunkDispatch } from '../../../types' import type { Fixture } from './constants' interface DeckSetupToolsProps { - cutoutId: CutoutId - slot: DeckSlotId onCloseClick: () => void + setHoveredLabware: (defUri: string | null) => void + onDeckProps: { + setHoveredModule: (model: ModuleModel | null) => void + setHoveredFixture: (fixture: Fixture | null) => void + } | null } -export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element { - const { slot, onCloseClick, cutoutId } = props - const { t } = useTranslation(['starting_deck_state', 'shared']) +export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { + const { onCloseClick, setHoveredLabware, onDeckProps } = props + const { t, i18n } = useTranslation(['starting_deck_state', 'shared']) + const { makeSnackbar } = useKitchen() + const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) const robotType = useSelector(getRobotType) const dispatch = useDispatch>() const enableAbsorbanceReader = useSelector(getEnableAbsorbanceReader) const enableMoam = useSelector(getEnableMoam) const deckSetup = useSelector(getDeckSetupForActiveItem) const { - labware: deckSetupLabware, + selectedLabwareDefUri, + selectedFixture, + selectedModuleModel, + selectedSlot, + selectedNestedLabwareDefUri, + } = selectedSlotInfo + const { slot, cutout } = selectedSlot + const [selectedHardware, setHardware] = React.useState< + ModuleModel | Fixture | null + >(null) + + // initialize the previously selected hardware because for some reason it does not + // work initiating it in the above useState + React.useEffect(() => { + if (selectedModuleModel || selectedFixture) { + setHardware(selectedModuleModel ?? selectedFixture ?? null) + } + }, [selectedModuleModel, selectedFixture]) + + const moduleModels = + slot != null + ? getModuleModelsBySlot(enableAbsorbanceReader, robotType, slot) + : null + const [tab, setTab] = React.useState<'hardware' | 'labware'>( + moduleModels?.length === 0 || slot === 'offDeck' ? 'labware' : 'hardware' + ) + + if (slot == null || (onDeckProps == null && slot !== 'offDeck')) { + return null + } + + const { modules: deckSetupModules, additionalEquipmentOnDeck, + labware: deckSetupLabware, } = deckSetup const hasTrash = Object.values(additionalEquipmentOnDeck).some( ae => ae.name === 'trashBin' ) - const createdModuleForSlot = Object.values(deckSetupModules).find( - module => module.slot === slot - ) - const createdLabwareForSlot = Object.values(deckSetupLabware).find( - lw => lw.slot === slot || lw.slot === createdModuleForSlot?.id - ) - const createdNestedLabwareForSlot = Object.values(deckSetupLabware).find(lw => - Object.keys(deckSetupLabware).includes(lw.slot) - ) - const createFixtureForSlots = Object.values(additionalEquipmentOnDeck).filter( - ae => ae.location?.split('cutout')[1] === slot - ) - - const preSelectedFixture = - createFixtureForSlots != null && createFixtureForSlots.length === 2 - ? ('wasteChuteAndStagingArea' as Fixture) - : (createFixtureForSlots[0]?.name as Fixture) - - const [selectedHardware, setHardware] = React.useState< - ModuleModel | Fixture | null - >(createdModuleForSlot?.model ?? preSelectedFixture ?? null) - const [selecteLabwareDefURI, setSelectedLabwareDefURI] = React.useState< - string | null - >(createdLabwareForSlot?.labwareDefURI ?? null) - const [ - nestedSelectedLabwareDefURI, - setNestedSelectedLabwareDefURI, - ] = React.useState( - createdNestedLabwareForSlot?.labwareDefURI ?? null - ) - const moduleModels = getModuleModelsBySlot( - enableAbsorbanceReader, - robotType, - slot - ) - const [tab, setTab] = React.useState<'hardware' | 'labware'>( - moduleModels.length === 0 ? 'labware' : 'hardware' - ) + const { + createdNestedLabwareForSlot, + createdModuleForSlot, + createdLabwareForSlot, + createFixtureForSlots, + } = getSlotInformation({ deckSetup, slot }) let fixtures: Fixture[] = [] if (slot === 'D3') { @@ -122,7 +131,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element { const hardwareTab = { text: t('deck_hardware'), - disabled: moduleModels.length === 0, + disabled: moduleModels?.length === 0, isActive: tab === 'hardware', onClick: () => { setTab('hardware') @@ -131,105 +140,123 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element { const labwareTab = { text: t('labware'), disabled: - selectedHardware === 'wasteChute' || - selectedHardware === 'wasteChuteAndStagingArea' || - selectedHardware === 'trashBin', + selectedFixture === 'wasteChute' || + selectedFixture === 'wasteChuteAndStagingArea' || + selectedFixture === 'trashBin', isActive: tab === 'labware', onClick: () => { setTab('labware') }, } + const handleResetToolbox = (): void => { + dispatch( + editSlotInfo({ + createdNestedLabwareForSlot: null, + createdLabwareForSlot: null, + createdModuleForSlot: null, + preSelectedFixture: null, + }) + ) + } + const handleClear = (): void => { - // clear module from slot - if (createdModuleForSlot != null) { - dispatch(deleteModule(createdModuleForSlot.id)) - } - // clear fixture(s) from slot - if (createFixtureForSlots.length > 0) { - createFixtureForSlots.forEach(fixture => - dispatch(deleteDeckFixture(fixture.id)) - ) - } - // clear labware from slot - if (createdLabwareForSlot != null) { - dispatch(deleteContainer({ labwareId: createdLabwareForSlot.id })) - } - // clear nested labware from slot - if (createdNestedLabwareForSlot != null) { - dispatch(deleteContainer({ labwareId: createdNestedLabwareForSlot.id })) + if (slot !== 'offDeck') { + // clear module from slot + if (createdModuleForSlot != null) { + dispatch(deleteModule(createdModuleForSlot.id)) + } + // clear fixture(s) from slot + if (createFixtureForSlots != null && createFixtureForSlots.length > 0) { + createFixtureForSlots.forEach(fixture => + dispatch(deleteDeckFixture(fixture.id)) + ) + } + // clear labware from slot + if (createdLabwareForSlot != null) { + dispatch(deleteContainer({ labwareId: createdLabwareForSlot.id })) + } + // clear nested labware from slot + if (createdNestedLabwareForSlot != null) { + dispatch(deleteContainer({ labwareId: createdNestedLabwareForSlot.id })) + } } + handleResetToolbox() + setHardware(null) } const handleConfirm = (): void => { // clear entities first before recreating them handleClear() - const fixture = FIXTURES.includes(selectedHardware as Fixture) - ? (selectedHardware as DeckFixture | 'wasteChuteAndStagingArea') - : undefined - const moduleModel = MODULE_MODELS.includes(selectedHardware as ModuleModel) - ? (selectedHardware as ModuleModel) - : undefined - if (fixture != null) { + if (selectedFixture != null && cutout != null) { // create fixture(s) - if (fixture === 'wasteChuteAndStagingArea') { - dispatch(createDeckFixture('wasteChute', cutoutId)) - dispatch(createDeckFixture('stagingArea', cutoutId)) + if (selectedFixture === 'wasteChuteAndStagingArea') { + dispatch(createDeckFixture('wasteChute', cutout)) + dispatch(createDeckFixture('stagingArea', cutout)) } else { - dispatch(createDeckFixture(fixture, cutoutId)) + dispatch(createDeckFixture(selectedFixture, cutout)) } } - if (moduleModel != null) { + if (selectedModuleModel != null) { // create module dispatch( createModule({ slot, - type: getModuleType(moduleModel), - model: moduleModel, + type: getModuleType(selectedModuleModel), + model: selectedModuleModel, }) ) } - if (moduleModel == null && selecteLabwareDefURI != null) { + if (selectedModuleModel == null && selectedLabwareDefUri != null) { // create adapter + labware on deck dispatch( createContainer({ slot, labwareDefURI: - nestedSelectedLabwareDefURI == null - ? selecteLabwareDefURI - : nestedSelectedLabwareDefURI, + selectedNestedLabwareDefUri == null + ? selectedLabwareDefUri + : selectedNestedLabwareDefUri, adapterUnderLabwareDefURI: - nestedSelectedLabwareDefURI == null + selectedNestedLabwareDefUri == null ? undefined - : selecteLabwareDefURI, + : selectedLabwareDefUri, }) ) } - if (moduleModel != null && selecteLabwareDefURI != null) { + if (selectedModuleModel != null && selectedLabwareDefUri != null) { // create adapter + labware on module dispatch( createContainerAboveModule({ slot, - labwareDefURI: selecteLabwareDefURI, - nestedLabwareDefURI: nestedSelectedLabwareDefURI ?? undefined, + labwareDefURI: selectedLabwareDefUri, + nestedLabwareDefURI: selectedNestedLabwareDefUri ?? undefined, }) ) } - + handleResetToolbox() + dispatch(selectZoomedIntoSlot({ slot: null, cutout: null })) onCloseClick() } - const handleResetToolbox = (): void => { - setHardware(null) - setSelectedLabwareDefURI(null) - setNestedSelectedLabwareDefURI(null) - } - return ( + + + {t('customize_slot')} + +
+ } closeButtonText={t('clear')} onCloseClick={() => { handleClear() @@ -241,7 +268,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element { confirmButtonText={t('done')} > - + {slot !== 'offDeck' ? : null} {tab === 'hardware' ? ( <> - {moduleModels.map(model => { + {moduleModels?.map(model => { const modelSomewhereOnDeck = Object.values( deckSetupModules ).filter( module => module.model === model && module.slot !== slot ) + const typeSomewhereOnDeck = Object.values( + deckSetupModules + ).filter( + module => + module.type === getModuleType(model) && module.slot !== slot + ) const moamModels = enableMoam ? MOAM_MODELS : MOAM_MODELS_WITH_FF - const maxMoamModel = - model === MAGNETIC_BLOCK_V1 - ? MAX_MAGNETIC_BLOCKS - : MAX_MOAM_MODULES + + const collisionError = getDeckErrors({ + modules: deckSetupModules, + selectedSlot: slot, + selectedModel: model, + labware: deckSetupLabware, + robotType: robotType, + }) return ( { + if (onDeckProps?.setHoveredModule != null) { + onDeckProps.setHoveredModule(null) + } + }} + setHovered={() => { + if (onDeckProps?.setHoveredModule != null) { + onDeckProps.setHoveredModule(model) + } + }} + largeDesktopBorderRadius + buttonLabel={ + + + + {getModuleDisplayName(model)} + + } - buttonLabel={getModuleDisplayName(model)} key={`${model}_${slot}`} buttonValue={model} onChange={() => { - setHardware(model) - setSelectedLabwareDefURI(null) + if ( + modelSomewhereOnDeck.length === 1 && + !moamModels.includes(model) && + robotType === FLEX_ROBOT_TYPE + ) { + makeSnackbar( + t('one_item', { + hardware: getModuleDisplayName(model), + }) as string + ) + } else if ( + typeSomewhereOnDeck.length > 0 && + robotType === OT2_ROBOT_TYPE + ) { + makeSnackbar( + t('one_item', { + hardware: t( + `shared:${getModuleType(model).toLowerCase()}` + ), + }) as string + ) + } else if (collisionError != null) { + makeSnackbar(t(`${collisionError}`) as string) + } else { + setHardware(model) + dispatch(selectModule({ moduleModel: model })) + dispatch(selectLabware({ labwareDefUri: null })) + dispatch( + selectNestedLabware({ nestedLabwareDefUri: null }) + ) + } }} isSelected={model === selectedHardware} /> @@ -301,14 +385,36 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element { {fixtures.map(fixture => ( { + if (onDeckProps?.setHoveredFixture != null) { + onDeckProps.setHoveredFixture(null) + } + }} + setHovered={() => { + if (onDeckProps?.setHoveredFixture != null) { + onDeckProps.setHoveredFixture(fixture) + } + }} + largeDesktopBorderRadius buttonLabel={t(`shared:${fixture}`)} key={`${fixture}_${slot}`} buttonValue={fixture} onChange={() => { - setHardware(fixture) - setSelectedLabwareDefURI(null) + // delete this when multiple trash bins are supported + if (fixture === 'trashBin' && hasTrash) { + makeSnackbar( + t('one_item', { + hardware: t('shared:trashBin'), + }) as string + ) + } else { + setHardware(fixture) + dispatch(selectFixture({ fixture: fixture })) + dispatch(selectLabware({ labwareDefUri: null })) + dispatch( + selectNestedLabware({ nestedLabwareDefUri: null }) + ) + } }} isSelected={fixture === selectedHardware} /> @@ -317,14 +423,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element { )} ) : ( - + )}
diff --git a/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx b/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx new file mode 100644 index 00000000000..be4130d785a --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import { + COLORS, + FlexTrash, + SingleSlotFixture, + StagingAreaFixture, + WasteChuteFixture, + WasteChuteStagingAreaFixture, +} from '@opentrons/components' +import { lightFill } from './DeckSetupContainer' +import type { TrashCutoutId, StagingAreaLocation } from '@opentrons/components' +import type { + CutoutId, + DeckDefinition, + RobotType, + WASTE_CHUTE_CUTOUT, +} from '@opentrons/shared-data' +import type { Fixture } from './constants' + +interface FixtureRenderProps { + fixture: Fixture + cutout: CutoutId + robotType: RobotType + deckDef: DeckDefinition +} +export const FixtureRender = (props: FixtureRenderProps): JSX.Element => { + const { fixture, cutout, deckDef, robotType } = props + + switch (fixture) { + case 'stagingArea': { + return ( + + ) + } + case 'trashBin': { + return ( + + + + + ) + } + case 'wasteChute': { + return ( + + ) + } + case 'wasteChuteAndStagingArea': { + return ( + + ) + } + } +} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/HoveredItems.tsx b/protocol-designer/src/pages/Designer/DeckSetup/HoveredItems.tsx new file mode 100644 index 00000000000..5f54c0967b7 --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/HoveredItems.tsx @@ -0,0 +1,133 @@ +import * as React from 'react' +import { useSelector } from 'react-redux' +import { LabwareRender, Module } from '@opentrons/components' +import { + getModuleDef2, + inferModuleOrientationFromXCoordinate, +} from '@opentrons/shared-data' +import { selectors } from '../../../labware-ingred/selectors' +import { getOnlyLatestDefs } from '../../../labware-defs' +import { getCustomLabwareDefsByURI } from '../../../labware-defs/selectors' +import { ModuleLabel } from './ModuleLabel' +import { LabwareLabel } from '../LabwareLabel' +import { FixtureRender } from './FixtureRender' +import type { DeckLabelProps } from '@opentrons/components' +import type { + CoordinateTuple, + DeckDefinition, + ModuleModel, + RobotType, +} from '@opentrons/shared-data' +import type { Fixture } from './constants' + +interface HoveredLabwareProps { + deckDef: DeckDefinition + robotType: RobotType + hoveredLabware: string | null + hoveredModule: ModuleModel | null + hoveredFixture: Fixture | null + hoveredSlotPosition: CoordinateTuple | null +} +export const HoveredItems = ( + props: HoveredLabwareProps +): JSX.Element | null => { + const { + deckDef, + robotType, + hoveredLabware, + hoveredModule, + hoveredFixture, + hoveredSlotPosition, + } = props + const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) + const { + selectedSlot, + selectedModuleModel, + selectedLabwareDefUri, + } = selectedSlotInfo + + const customLabwareDefs = useSelector(getCustomLabwareDefsByURI) + const defs = getOnlyLatestDefs() + + if (hoveredSlotPosition == null) { + return null + } + const hoveredModuleDef = + hoveredModule != null ? getModuleDef2(hoveredModule) : hoveredModule + const hoveredLabwareDef = + hoveredLabware != null + ? defs[hoveredLabware] ?? customLabwareDefs[hoveredLabware] ?? null + : null + + const orientation = + hoveredSlotPosition != null + ? inferModuleOrientationFromXCoordinate(hoveredSlotPosition[0]) + : null + + const nestedInfo: DeckLabelProps[] = + selectedLabwareDefUri != null && + (hoveredLabware == null || hoveredLabware !== selectedLabwareDefUri) + ? [ + { + text: defs[selectedLabwareDefUri].metadata.displayName, + isLast: false, + isSelected: true, + }, + ] + : [] + + return ( + <> + {hoveredFixture != null && selectedSlot.cutout != null ? ( + + ) : null} + {hoveredModuleDef != null && + hoveredSlotPosition != null && + orientation != null ? ( + <> + + {hoveredModule != null ? ( + + ) : null} + + ) : null} + + {hoveredLabwareDef != null && + hoveredSlotPosition != null && + hoveredLabware != null && + selectedModuleModel == null ? ( + + + + + + + ) : null} + + ) +} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx index 201593ed7c9..9da8dfab291 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx @@ -4,10 +4,13 @@ import reduce from 'lodash/reduce' import styled from 'styled-components' import { useDispatch, useSelector } from 'react-redux' import { - COLORS, + ALIGN_CENTER, + CheckboxField, DIRECTION_COLUMN, DISPLAY_INLINE_BLOCK, Flex, + InputField, + JUSTIFY_CENTER, ListButton, ListButtonAccordion, ListButtonAccordionContainer, @@ -17,9 +20,9 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + ABSORBANCE_READER_TYPE, HEATERSHAKER_MODULE_TYPE, MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM, - MODULE_MODELS, OT2_ROBOT_TYPE, getAreSlotsHorizontallyAdjacent, getIsLabwareAboveHeight, @@ -28,6 +31,7 @@ import { getModuleType, } from '@opentrons/shared-data' +import { BUTTON_LINK_STYLE } from '../../../atoms' import { selectors as stepFormSelectors } from '../../../step-forms' import { getOnlyLatestDefs } from '../../../labware-defs' import { @@ -39,86 +43,75 @@ import { createCustomLabwareDef } from '../../../labware-defs/actions' import { getRobotType } from '../../../file-data/selectors' import { getCustomLabwareDefsByURI } from '../../../labware-defs/selectors' import { getPipetteEntities } from '../../../step-forms/selectors' +import { selectors } from '../../../labware-ingred/selectors' +import { + selectLabware, + selectNestedLabware, +} from '../../../labware-ingred/actions' import { ORDERED_CATEGORIES } from './constants' import { getLabwareIsRecommended, getLabwareCompatibleWithAdapter, } from './utils' -import type { - DeckSlotId, - LabwareDefinition2, - ModuleModel, -} from '@opentrons/shared-data' +import type { DeckSlotId, LabwareDefinition2 } from '@opentrons/shared-data' import type { ModuleOnDeck } from '../../../step-forms' import type { ThunkDispatch } from '../../../types' import type { LabwareDefByDefURI } from '../../../labware-defs' -import type { Fixture } from './constants' const CUSTOM_CATEGORY = 'custom' const STANDARD_X_DIMENSION = 127.75 const STANDARD_Y_DIMENSION = 85.48 +const PLATE_READER_LOADNAME = + 'opentrons_flex_lid_absorbance_plate_reader_module' interface LabwareToolsProps { slot: DeckSlotId - selectedHardware: ModuleModel | Fixture | null - setSelectedLabwareDefURI: React.Dispatch> - selecteLabwareDefURI: string | null - setNestedSelectedLabwareDefURI: React.Dispatch< - React.SetStateAction - > - selectedNestedSelectedLabwareDefURI: string | null + setHoveredLabware: (defUri: string | null) => void } export function LabwareTools(props: LabwareToolsProps): JSX.Element { - const { - slot, - selectedHardware, - setSelectedLabwareDefURI, - selecteLabwareDefURI, - setNestedSelectedLabwareDefURI, - selectedNestedSelectedLabwareDefURI, - } = props + const { slot, setHoveredLabware } = props const { t } = useTranslation(['starting_deck_state', 'shared']) const robotType = useSelector(getRobotType) const dispatch = useDispatch>() const permittedTipracks = useSelector(stepFormSelectors.getPermittedTipracks) const pipetteEntities = useSelector(getPipetteEntities) const customLabwareDefs = useSelector(getCustomLabwareDefsByURI) + const has96Channel = getHas96Channel(pipetteEntities) + const defs = getOnlyLatestDefs() const deckSetup = useSelector(stepFormSelectors.getInitialDeckSetup) - // TODO(ja, 8/16/24): We are always filtering recommended labware, check with designs - // where to add the filter checkbox/button - const [filterRecommended, setFilterRecommended] = React.useState( - true - ) + const zoomedInSlotInfo = useSelector(selectors.getZoomedInSlotInfo) + const { + selectedLabwareDefUri, + selectedModuleModel, + selectedNestedLabwareDefUri, + } = zoomedInSlotInfo const [selectedCategory, setSelectedCategory] = React.useState( null ) - const [filterHeight, setFilterHeight] = React.useState(false) + const [searchTerm, setSearchTerm] = React.useState('') - const has96Channel = getHas96Channel(pipetteEntities) - const defs = getOnlyLatestDefs() - const modulesById = deckSetup.modules - const moduleModel = MODULE_MODELS.includes(selectedHardware as ModuleModel) - ? (selectedHardware as ModuleModel) - : null + const searchFilter = (termToCheck: string): boolean => + termToCheck.toLowerCase().includes(searchTerm.toLowerCase()) - const moduleType = moduleModel != null ? getModuleType(moduleModel) : null + const modulesById = deckSetup.modules + const moduleType = + selectedModuleModel != null ? getModuleType(selectedModuleModel) : null const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map( moduleId => modulesById[moduleId] ) + const [filterRecommended, setFilterRecommended] = React.useState( + moduleType != null + ) // for OT-2 usage only due to H-S collisions const isNextToHeaterShaker = initialModules.some( hardwareModule => hardwareModule.type === HEATERSHAKER_MODULE_TYPE && getAreSlotsHorizontallyAdjacent(hardwareModule.slot, slot) ) - // if you're adding labware to a module, check the recommended filter by default - React.useEffect(() => { - setFilterRecommended(moduleType != null) - if (robotType === OT2_ROBOT_TYPE) { - setFilterHeight(isNextToHeaterShaker) - } - }, [moduleType, isNextToHeaterShaker, robotType]) + const [filterHeight, setFilterHeight] = React.useState( + robotType === OT2_ROBOT_TYPE ? isNextToHeaterShaker : false + ) const getLabwareCompatible = React.useCallback( (def: LabwareDefinition2) => { @@ -139,13 +132,11 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { const isSmallXDimension = xDimension < STANDARD_X_DIMENSION const isSmallYDimension = yDimension < STANDARD_Y_DIMENSION const isIrregularSize = isSmallXDimension && isSmallYDimension - const isAdapter = labwareDef.allowedRoles?.includes('adapter') const isAdapter96Channel = parameters.loadName === ADAPTER_96_CHANNEL - return ( (filterRecommended && - !getLabwareIsRecommended(labwareDef, moduleModel)) || + !getLabwareIsRecommended(labwareDef, selectedModuleModel)) || (filterHeight && getIsLabwareAboveHeight( labwareDef, @@ -156,7 +147,9 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { isIrregularSize && moduleType !== HEATERSHAKER_MODULE_TYPE) || (isAdapter96Channel && !has96Channel) || - (slot === 'offDeck' && isAdapter) + (slot === 'offDeck' && isAdapter) || + (PLATE_READER_LOADNAME === parameters.loadName && + moduleType !== ABSORBANCE_READER_TYPE) ) }, [filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot] @@ -192,205 +185,298 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { const populatedCategories: { [category: string]: boolean } = React.useMemo( () => - ORDERED_CATEGORIES.reduce( - (acc, category) => - labwareByCategory[category] - ? { - ...acc, - [category]: labwareByCategory[category].some( - def => !getIsLabwareFiltered(def) - ), - } - : acc, - {} - ), - [labwareByCategory, getIsLabwareFiltered] + ORDERED_CATEGORIES.reduce((acc, category) => { + const isDeckLocationCategory = + slot === 'offDeck' ? category !== 'adapter' : true + return category in labwareByCategory && + isDeckLocationCategory && + labwareByCategory[category].some(lw => + searchFilter(lw.metadata.displayName) + ) + ? { + ...acc, + [category]: labwareByCategory[category].some( + def => !getIsLabwareFiltered(def) + ), + } + : acc + }, {}), + [labwareByCategory, getIsLabwareFiltered, searchTerm] ) + const handleCategoryClick = (category: string): void => { + setSelectedCategory(selectedCategory === category ? null : category) + } return ( - - TODO: add search bar - + + {t('add_labware')} - - {customLabwareURIs.length === 0 ? null : ( - { - setSelectedCategory(CUSTOM_CATEGORY) + { + setSearchTerm(e.target.value) + }} + placeholder="Search for labware..." + size="medium" + leftIcon="search" + showDeleteIcon + onDelete={() => { + setSearchTerm('') }} - > - - - {customLabwareURIs.map((labwareURI, index) => ( - { - e.stopPropagation() - setSelectedLabwareDefURI(labwareURI) - }} - isSelected={labwareURI === selecteLabwareDefURI} - /> - ))} - - - - )} - {ORDERED_CATEGORIES.map(category => { - const isPopulated = populatedCategories[category] - if (isPopulated) { - return ( - { - setSelectedCategory(category) + /> + {moduleType != null || + (isNextToHeaterShaker && robotType === OT2_ROBOT_TYPE) ? ( + + ) => { + isNextToHeaterShaker + ? setFilterHeight(e.currentTarget.checked) + : setFilterRecommended(e.currentTarget.checked) }} - > - - - {labwareByCategory[category]?.map((labwareDef, index) => { - const isFiltered = getIsLabwareFiltered(labwareDef) - const labwareURI = getLabwareDefURI(labwareDef) - const loadName = labwareDef.parameters.loadName - - if (!isFiltered) { - return ( - - { - e.stopPropagation() - setSelectedLabwareDefURI(labwareURI) - }} - isSelected={labwareURI === selecteLabwareDefURI} - /> + value={ + isNextToHeaterShaker && robotType === OT2_ROBOT_TYPE + ? filterHeight + : filterRecommended + } + /> + + {t('only_display_rec')} + + + ) : null} + + + {customLabwareURIs.length === 0 ? null : ( + { + handleCategoryClick(CUSTOM_CATEGORY) + }} + > + + + {customLabwareURIs.map((labwareURI, index) => ( + { + setHoveredLabware(null) + }} + setHovered={() => { + setHoveredLabware(labwareURI) + }} + buttonValue={labwareURI} + onChange={e => { + e.stopPropagation() + dispatch(selectLabware({ labwareDefUri: labwareURI })) + }} + isSelected={labwareURI === selectedLabwareDefUri} + /> + ))} + + + + )} + {ORDERED_CATEGORIES.map(category => { + const isPopulated = populatedCategories[category] + if (isPopulated) { + return ( + { + handleCategoryClick(category) + }} + > + + + {labwareByCategory[category]?.map((labwareDef, index) => { + const isFiltered = getIsLabwareFiltered(labwareDef) + const labwareURI = getLabwareDefURI(labwareDef) + const loadName = labwareDef.parameters.loadName + const isMatch = searchFilter( + labwareDef.metadata.displayName + ) + if (!isFiltered && isMatch) { + return ( + + { + setHoveredLabware(null) + }} + setHovered={() => { + setHoveredLabware(labwareURI) + }} + id={`${index}_${category}_${loadName}`} + buttonText={labwareDef.metadata.displayName} + buttonValue={labwareURI} + onChange={e => { + e.stopPropagation() + dispatch( + selectLabware({ + labwareDefUri: + labwareURI === selectedLabwareDefUri + ? null + : labwareURI, + }) + ) + // reset the nested labware def uri in case it is not compatible + dispatch( + selectNestedLabware({ + nestedLabwareDefUri: null, + }) + ) + }} + isSelected={labwareURI === selectedLabwareDefUri} + /> - {labwareURI === selecteLabwareDefURI && - getLabwareCompatibleWithAdapter(loadName)?.length > - 0 && ( - - 0 && ( + - {has96Channel && - loadName === ADAPTER_96_CHANNEL - ? permittedTipracks.map( - (tiprackDefUri, index) => { - const nestedDef = defs[tiprackDefUri] + + {has96Channel && + loadName === ADAPTER_96_CHANNEL + ? permittedTipracks.map( + (tiprackDefUri, index) => { + const nestedDef = + defs[tiprackDefUri] + return ( + { + setHoveredLabware(null) + }} + setHovered={() => { + setHoveredLabware( + tiprackDefUri + ) + }} + key={`${index}_${category}_${loadName}_${tiprackDefUri}`} + id={`${index}_${category}_${loadName}_${tiprackDefUri}`} + buttonText={ + nestedDef?.metadata + .displayName ?? '' + } + buttonValue={tiprackDefUri} + onChange={e => { + e.stopPropagation() + dispatch( + selectNestedLabware({ + nestedLabwareDefUri: tiprackDefUri, + }) + ) + }} + isSelected={ + tiprackDefUri === + selectedNestedLabwareDefUri + } + /> + ) + } + ) + : getLabwareCompatibleWithAdapter( + loadName + ).map(nestedDefUri => { + const nestedDef = defs[nestedDefUri] + return ( { + setHoveredLabware(null) + }} + setHovered={() => { + setHoveredLabware(nestedDefUri) + }} + key={`${index}_${category}_${loadName}_${nestedDefUri}`} + id={`${index}_${category}_${loadName}_${nestedDefUri}`} buttonText={ nestedDef?.metadata .displayName ?? '' } - buttonValue={tiprackDefUri} + buttonValue={nestedDefUri} onChange={e => { e.stopPropagation() - setNestedSelectedLabwareDefURI( - tiprackDefUri + dispatch( + selectNestedLabware({ + nestedLabwareDefUri: nestedDefUri, + }) ) }} isSelected={ - tiprackDefUri === - selectedNestedSelectedLabwareDefURI + nestedDefUri === + selectedNestedLabwareDefUri } /> ) - } - ) - : getLabwareCompatibleWithAdapter( - loadName - ).map(nestedDefUri => { - const nestedDef = defs[nestedDefUri] - - return ( - { - e.stopPropagation() - setNestedSelectedLabwareDefURI( - nestedDefUri - ) - }} - isSelected={ - nestedDefUri === - selectedNestedSelectedLabwareDefURI - } - /> - ) - })} - - - )} - - ) - } - })} - - - - ) - } - })} - - - {t('custom_labware')} - - { - setSelectedCategory(CUSTOM_CATEGORY) - dispatch(createCustomLabwareDef(e)) - }} - /> - + })} + + + )} + + ) + } + })} + + + + ) + } + })} + + + + + {t('upload_custom_labware')} + + { + setSelectedCategory(CUSTOM_CATEGORY) + dispatch(createCustomLabwareDef(e)) + }} + /> + + ) } const StyledLabel = styled.label` text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; - text-align: ${TYPOGRAPHY.textAlignCenter}}; - display: ${DISPLAY_INLINE_BLOCK} + text-align: ${TYPOGRAPHY.textAlignCenter}; + display: ${DISPLAY_INLINE_BLOCK}; cursor: pointer; input[type='file'] { display: none; diff --git a/protocol-designer/src/pages/Designer/DeckSetup/ModuleLabel.tsx b/protocol-designer/src/pages/Designer/DeckSetup/ModuleLabel.tsx new file mode 100644 index 00000000000..e527a001d2f --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/ModuleLabel.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' +import { DeckLabelSet } from '@opentrons/components' +import { + HEATERSHAKER_MODULE_TYPE, + MAGNETIC_MODULE_TYPE, + TEMPERATURE_MODULE_TYPE, + getModuleDef2, +} from '@opentrons/shared-data' +import type { DeckLabelProps } from '@opentrons/components' +import type { CoordinateTuple, ModuleModel } from '@opentrons/shared-data' + +interface ModuleLabelProps { + moduleModel: ModuleModel + position: CoordinateTuple + orientation: 'left' | 'right' + isSelected: boolean + isLast: boolean + labwareInfos?: DeckLabelProps[] +} +export const ModuleLabel = (props: ModuleLabelProps): JSX.Element => { + const { + moduleModel, + position, + orientation, + isSelected, + isLast, + labwareInfos = [], + } = props + const labelContainerRef = React.useRef(null) + const [labelContainerHeight, setLabelContainerHeight] = React.useState(12) + + React.useEffect(() => { + if (labelContainerRef.current) { + setLabelContainerHeight(labelContainerRef.current.offsetHeight) + } + }, [labwareInfos]) + + const def = getModuleDef2(moduleModel) + const overhang = + def?.dimensions.labwareInterfaceXDimension != null + ? def.dimensions.xDimension - def?.dimensions.labwareInterfaceXDimension + : 0 + // TODO(ja 9/6/24): definitely need to refine these overhang values + let leftOverhang = overhang + if (def?.moduleType === TEMPERATURE_MODULE_TYPE) { + leftOverhang = overhang * 2 + } else if (def?.moduleType === HEATERSHAKER_MODULE_TYPE) { + leftOverhang = overhang + 14 + } else if (def?.moduleType === MAGNETIC_MODULE_TYPE) { + leftOverhang = overhang + 8 + } + + return ( + + ) +} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx new file mode 100644 index 00000000000..5982e5b521b --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx @@ -0,0 +1,206 @@ +import * as React from 'react' +import { useSelector } from 'react-redux' +import { FixtureRender } from './FixtureRender' +import { LabwareRender, Module } from '@opentrons/components' +import { + getModuleDef2, + inferModuleOrientationFromXCoordinate, +} from '@opentrons/shared-data' +import { selectors } from '../../../labware-ingred/selectors' +import { getOnlyLatestDefs } from '../../../labware-defs' +import { getCustomLabwareDefsByURI } from '../../../labware-defs/selectors' +import { ModuleLabel } from './ModuleLabel' +import { LabwareLabel } from '../LabwareLabel' +import type { + CoordinateTuple, + DeckDefinition, + ModuleModel, + RobotType, +} from '@opentrons/shared-data' +import type { DeckLabelProps } from '@opentrons/components' +import type { Fixture } from './constants' + +interface SelectedHoveredItemsProps { + deckDef: DeckDefinition + robotType: RobotType + hoveredLabware: string | null + hoveredModule: ModuleModel | null + hoveredFixture: Fixture | null + slotPosition: CoordinateTuple | null +} +export const SelectedHoveredItems = ( + props: SelectedHoveredItemsProps +): JSX.Element => { + const { + deckDef, + robotType, + hoveredFixture, + hoveredModule, + hoveredLabware, + slotPosition, + } = props + const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) + const { + selectedSlot, + selectedFixture, + selectedLabwareDefUri, + selectedModuleModel, + selectedNestedLabwareDefUri, + } = selectedSlotInfo + const customLabwareDefs = useSelector(getCustomLabwareDefsByURI) + const defs = getOnlyLatestDefs() + + const hoveredLabwareDef = + hoveredLabware != null + ? defs[hoveredLabware] ?? customLabwareDefs[hoveredLabware] ?? null + : null + const orientation = + slotPosition != null + ? inferModuleOrientationFromXCoordinate(slotPosition[0]) + : null + + const labwareInfos: DeckLabelProps[] = [] + + if ( + selectedLabwareDefUri != null && + (hoveredLabware == null || hoveredLabware !== selectedLabwareDefUri) + ) { + const def = defs[selectedLabwareDefUri] + const selectedLabwareLabel = { + text: def.metadata.displayName, + isSelected: true, + isLast: hoveredLabware == null && selectedNestedLabwareDefUri == null, + } + labwareInfos.push(selectedLabwareLabel) + } + if (selectedNestedLabwareDefUri != null && hoveredLabware == null) { + const def = defs[selectedNestedLabwareDefUri] + const selectedNestedLabwareLabel = { + text: def.metadata.displayName, + isSelected: true, + isLast: hoveredLabware == null, + } + labwareInfos.push(selectedNestedLabwareLabel) + } + if ( + (hoveredLabware != null || + selectedLabwareDefUri === hoveredLabware || + selectedNestedLabwareDefUri === hoveredLabware) && + hoveredLabwareDef != null + ) { + const hoverLabelLabel = { + text: hoveredLabwareDef.metadata.displayName, + isSelected: false, + isLast: true, + } + labwareInfos.push(hoverLabelLabel) + } + + return ( + <> + {selectedFixture != null && + selectedSlot.cutout != null && + hoveredFixture == null && + hoveredModule == null ? ( + + ) : null} + {selectedModuleModel != null && + slotPosition != null && + hoveredModule == null && + hoveredFixture == null && + orientation != null ? ( + <> + + <> + {selectedLabwareDefUri != null && + selectedModuleModel != null && + hoveredLabware == null ? ( + + + + ) : null} + {selectedNestedLabwareDefUri != null && + selectedModuleModel != null && + hoveredLabware == null ? ( + + + + ) : null} + {hoveredLabwareDef != null && selectedModuleModel != null ? ( + + + + ) : null} + + + {selectedModuleModel != null ? ( + + ) : null} + + ) : null} + {selectedLabwareDefUri != null && + slotPosition != null && + selectedModuleModel == null && + hoveredLabware == null ? ( + <> + + + + {selectedNestedLabwareDefUri == null ? ( + + ) : null} + + ) : null} + {selectedNestedLabwareDefUri != null && + slotPosition != null && + selectedModuleModel == null && + hoveredLabware == null ? ( + <> + + + + {selectedLabwareDefUri != null ? ( + + ) : null} + + ) : null} + + ) +} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SlotDetailsContainer.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SlotDetailsContainer.tsx deleted file mode 100644 index 6cdbb1333cb..00000000000 --- a/protocol-designer/src/pages/Designer/DeckSetup/SlotDetailsContainer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react' -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' -import { - LegacyStyledText, - RobotCoordsForeignObject, -} from '@opentrons/components' - -import type { RobotType } from '@opentrons/shared-data' - -interface SlotDetailContainerProps { - robotType: RobotType -} - -export const SlotDetailsContainer = ( - props: SlotDetailContainerProps -): JSX.Element | null => { - const { robotType } = props - return ( - - {/* TODO(ja, 8/6/24): wire up slot information */} - Slot information - - ) -} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx new file mode 100644 index 00000000000..610683b014b --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx @@ -0,0 +1,289 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + NO_WRAP, + POSITION_ABSOLUTE, + RobotCoordsForeignDiv, + SPACING, + StyledText, + useOnClickOutside, +} from '@opentrons/components' +import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' +import { deleteModule } from '../../../step-forms/actions' +import { EditNickNameModal } from '../../../organisms' +import { deleteDeckFixture } from '../../../step-forms/actions/additionalItems' +import { + deleteContainer, + duplicateLabware, + openIngredientSelector, +} from '../../../labware-ingred/actions' +import type { CoordinateTuple, DeckSlotId } from '@opentrons/shared-data' +import type { ThunkDispatch } from '../../../types' + +const ROBOT_BOTTOM_HALF_SLOTS = [ + 'D1', + 'D2', + 'D3', + 'D4', + 'C1', + 'C2', + 'C3', + 'C4', + '1', + '2', + '3', + '4', + '5', + '6', +] +const BOTTOM_SLOT_Y_POSITION = -70 +const TOP_SLOT_Y_POSITION = 50 +const TOP_SLOT_Y_POSITION_ALL_BUTTONS = 110 +const TOP_SLOT_Y_POSITION_2_BUTTONS = 35 + +interface SlotOverflowMenuProps { + // can be off-deck id or deck slot + location: DeckSlotId | string + setShowMenuList: (value: React.SetStateAction) => void + addEquipment: (slotId: string) => void + menuListSlotPosition?: CoordinateTuple +} +export function SlotOverflowMenu( + props: SlotOverflowMenuProps +): JSX.Element | null { + const { + location, + setShowMenuList, + addEquipment, + menuListSlotPosition, + } = props + const { t } = useTranslation('starting_deck_state') + const navigate = useNavigate() + const dispatch = useDispatch>() + const [showNickNameModal, setShowNickNameModal] = React.useState( + false + ) + const overflowWrapperRef = useOnClickOutside({ + onClickOutside: () => { + if (!showNickNameModal) { + setShowMenuList(false) + } + }, + }) + const deckSetup = useSelector(getDeckSetupForActiveItem) + const { + labware: deckSetupLabware, + modules: deckSetupModules, + additionalEquipmentOnDeck, + } = deckSetup + const isOffDeckLocation = deckSetupLabware[location] != null + + const moduleOnSlot = Object.values(deckSetupModules).find( + module => module.slot === location + ) + const labwareOnSlot = Object.values(deckSetupLabware).find(lw => + isOffDeckLocation + ? lw.id === location + : lw.slot === location || lw.slot === moduleOnSlot?.id + ) + const isLabwareTiprack = labwareOnSlot?.def.parameters.isTiprack ?? false + const isLabwareAnAdapter = + labwareOnSlot?.def.allowedRoles?.includes('adapter') ?? false + const nestedLabwareOnSlot = Object.values(deckSetupLabware).find( + lw => lw.slot === labwareOnSlot?.id + ) + const fixturesOnSlot = Object.values(additionalEquipmentOnDeck).filter( + ae => ae.location?.split('cutout')[1] === location + ) + + const hasNoItems = + moduleOnSlot == null && labwareOnSlot == null && fixturesOnSlot.length === 0 + + const handleClear = (): void => { + // clear module from slot + if (moduleOnSlot != null) { + dispatch(deleteModule(moduleOnSlot.id)) + } + // clear fixture(s) from slot + if (fixturesOnSlot.length > 0) { + fixturesOnSlot.forEach(fixture => dispatch(deleteDeckFixture(fixture.id))) + } + // clear labware from slot + if (labwareOnSlot != null) { + dispatch(deleteContainer({ labwareId: labwareOnSlot.id })) + } + // clear nested labware from slot + if (nestedLabwareOnSlot != null) { + dispatch(deleteContainer({ labwareId: nestedLabwareOnSlot.id })) + } + } + const showDuplicateBtn = + (labwareOnSlot != null && + !isLabwareAnAdapter && + nestedLabwareOnSlot == null) || + nestedLabwareOnSlot != null + + const showEditAndLiquidsBtns = + (labwareOnSlot != null && + !isLabwareAnAdapter && + !isLabwareTiprack && + nestedLabwareOnSlot == null) || + nestedLabwareOnSlot != null + + let position = ROBOT_BOTTOM_HALF_SLOTS.includes(location) + ? BOTTOM_SLOT_Y_POSITION + : TOP_SLOT_Y_POSITION + + if (showDuplicateBtn && !ROBOT_BOTTOM_HALF_SLOTS.includes(location)) { + position += showEditAndLiquidsBtns + ? TOP_SLOT_Y_POSITION_ALL_BUTTONS + : TOP_SLOT_Y_POSITION_2_BUTTONS + } + + let nickNameId = labwareOnSlot?.id + if (nestedLabwareOnSlot != null) { + nickNameId = nestedLabwareOnSlot.id + } else if (isOffDeckLocation) { + nickNameId = location + } + const slotOverflowBody = ( + <> + {showNickNameModal && nickNameId != null ? ( + { + setShowNickNameModal(false) + setShowMenuList(false) + }} + /> + ) : null} + { + e.preventDefault() + e.stopPropagation() + }} + > + { + addEquipment(location) + setShowMenuList(false) + }} + > + + {hasNoItems + ? t(isOffDeckLocation ? 'add_labware' : 'add_hw_lw') + : t(isOffDeckLocation ? 'edit_labware' : 'edit_hw_lw')} + + + {showEditAndLiquidsBtns ? ( + <> + { + setShowNickNameModal(true) + e.preventDefault() + e.stopPropagation() + }} + > + + {t('rename_lab')} + + + { + if (nestedLabwareOnSlot != null) { + dispatch(openIngredientSelector(nestedLabwareOnSlot.id)) + } else if (labwareOnSlot != null) { + dispatch(openIngredientSelector(labwareOnSlot.id)) + } + navigate('/liquids') + }} + > + + {t('add_liquid')} + + + + ) : null} + {showDuplicateBtn ? ( + { + if ( + labwareOnSlot != null && + !isLabwareAnAdapter && + nestedLabwareOnSlot == null + ) { + dispatch(duplicateLabware(labwareOnSlot.id)) + } else if (nestedLabwareOnSlot != null) { + dispatch(duplicateLabware(nestedLabwareOnSlot.id)) + } + setShowMenuList(false) + }} + > + + {t('duplicate')} + + + ) : null} + { + handleClear() + setShowMenuList(false) + }} + > + + {t(isOffDeckLocation ? 'clear_labware' : 'clear_slot')} + + + + + ) + + return menuListSlotPosition != null ? ( + + {slotOverflowBody} + + ) : ( + slotOverflowBody + ) +} + +const MenuButton = styled.button` + background-color: ${COLORS.transparent}; + border-radius: inherit; + cursor: pointer; + padding: ${SPACING.spacing8} ${SPACING.spacing12}; + border: none; + border-radius: inherit; + &:hover { + background-color: ${COLORS.blue10}; + } + &:disabled { + color: ${COLORS.grey40}; + cursor: auto; + } +` diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupContainer.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupContainer.test.tsx index f43fddfe877..86c45b00d62 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupContainer.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupContainer.test.tsx @@ -1,3 +1,97 @@ -import { it } from 'vitest' +import * as React from 'react' +import { describe, it, vi, beforeEach, expect } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { screen } from '@testing-library/react' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { FlexTrash } from '@opentrons/components' -it.todo('write test for DeckSetupContainer') +import { renderWithProviders } from '../../../../__testing-utils__' + +import { selectors } from '../../../../labware-ingred/selectors' +import { getDeckSetupForActiveItem } from '../../../../top-selectors/labware-locations' +import { DeckSetupTools } from '../DeckSetupTools' +import { DeckSetupContainer } from '../DeckSetupContainer' +import { getSelectedTerminalItemId } from '../../../../ui/steps' +import { getDisableModuleRestrictions } from '../../../../feature-flags/selectors' +import { getRobotType } from '../../../../file-data/selectors' +import { DeckSetupDetails } from '../DeckSetupDetails' +import type * as OpentronsComponents from '@opentrons/components' + +vi.mock('../../../../top-selectors/labware-locations') +vi.mock('../../../../feature-flags/selectors') +vi.mock('../DeckSetupTools') +vi.mock('../DeckSetupDetails') +vi.mock('../../../../ui/steps') +vi.mock('../../../../labware-ingred/selectors') +vi.mock('../../../../file-data/selectors') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + FlexTrash: vi.fn(), + } +}) + +const render = () => { + return renderWithProviders()[0] +} + +describe('DeckSetupContainer', () => { + beforeEach(() => { + vi.mocked(selectors.getZoomedInSlot).mockReturnValue({ + slot: 'D3', + cutout: 'cutoutD3', + }) + vi.mocked(DeckSetupTools).mockReturnValue(
mock DeckSetupTools
) + vi.mocked(DeckSetupDetails).mockReturnValue( +
mock DeckSetupDetails
+ ) + vi.mocked(FlexTrash).mockReturnValue(
mock FlexTrash
) + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + vi.mocked(getDisableModuleRestrictions).mockReturnValue(false) + vi.mocked(getSelectedTerminalItemId).mockReturnValue('__initial_setup__') + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + labware: {}, + modules: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + }) + }) + it('renders the decksetupTools when slot and cutout are not null', () => { + render() + screen.getByText('mock DeckSetupDetails') + screen.getByText('mock DeckSetupTools') + }) + it('renders no deckSetupTools when slot and cutout are null', () => { + vi.mocked(selectors.getZoomedInSlot).mockReturnValue({ + slot: null, + cutout: null, + }) + render() + screen.getByText('mock DeckSetupDetails') + }) + it('renders a flex trash when a trash bin is attached', () => { + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + labware: {}, + modules: {}, + additionalEquipmentOnDeck: { + trash: { name: 'trashBin', location: 'cutoutA3', id: 'mockId' }, + }, + pipettes: {}, + }) + render() + screen.getByText('mock FlexTrash') + }) + it('does not render a flex trash if the zoomed in slot cutout is the same location', () => { + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + labware: {}, + modules: {}, + additionalEquipmentOnDeck: { + trash: { name: 'trashBin', location: 'cutoutD3', id: 'mockId' }, + }, + pipettes: {}, + }) + render() + expect(screen.queryByText('mock FlexTrash')).not.toBeInTheDocument() + }) +}) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index a4fc74466e3..d8872d540b8 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -2,7 +2,11 @@ import * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' -import { FLEX_ROBOT_TYPE, fixture96Plate } from '@opentrons/shared-data' +import { + FLEX_ROBOT_TYPE, + HEATERSHAKER_MODULE_V1, + fixture96Plate, +} from '@opentrons/shared-data' import { i18n } from '../../../../assets/localization' import { renderWithProviders } from '../../../../__testing-utils__' import { deleteContainer } from '../../../../labware-ingred/actions' @@ -16,20 +20,21 @@ import { createDeckFixture, deleteDeckFixture, } from '../../../../step-forms/actions/additionalItems' +import { selectors } from '../../../../labware-ingred/selectors' import { getDeckSetupForActiveItem } from '../../../../top-selectors/labware-locations' import { DeckSetupTools } from '../DeckSetupTools' import { LabwareTools } from '../LabwareTools' import type { LabwareDefinition2 } from '@opentrons/shared-data' +vi.mock('../LabwareTools') vi.mock('../../../../feature-flags/selectors') vi.mock('../../../../file-data/selectors') vi.mock('../../../../top-selectors/labware-locations') -vi.mock('../LabwareTools') vi.mock('../../../../labware-ingred/actions') vi.mock('../../../../step-forms/actions') vi.mock('../../../../step-forms/actions/additionalItems') - +vi.mock('../../../../labware-ingred/selectors') const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -41,10 +46,20 @@ describe('DeckSetupTools', () => { beforeEach(() => { props = { - cutoutId: 'cutoutD3', - slot: 'D3', onCloseClick: vi.fn(), + setHoveredLabware: vi.fn(), + onDeckProps: { + setHoveredModule: vi.fn(), + setHoveredFixture: vi.fn(), + }, } + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: null, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) vi.mocked(LabwareTools).mockReturnValue(
mock labware tools
) vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) vi.mocked(getEnableAbsorbanceReader).mockReturnValue(true) @@ -60,7 +75,8 @@ describe('DeckSetupTools', () => { render(props) screen.getByText('Add a module') screen.getByText('Add a fixture') - screen.getByText('Customize slot D3') + screen.getByTestId('DeckInfoLabel_D3') + screen.getByText('Customize slot') screen.getByText('Deck hardware') screen.getByText('Labware') screen.getByText('Absorbance Plate Reader Module GEN1') @@ -69,8 +85,8 @@ describe('DeckSetupTools', () => { screen.getByText('Temperature Module GEN2') screen.getByText('Staging area') screen.getByText('Waste chute') - screen.getByText('Trash bin') - screen.getByText('Waste chute and staging area') + screen.getByText('Trash Bin') + screen.getByText('Waste chute and staging area slot') }) it('should render the labware tab', () => { render(props) @@ -116,6 +132,13 @@ describe('DeckSetupTools', () => { expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalled() }) it('should close and add h-s module when done is called', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: HEATERSHAKER_MODULE_V1, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) render(props) fireEvent.click(screen.getByText('Heater-Shaker Module GEN1')) fireEvent.click(screen.getByText('Done')) @@ -123,8 +146,15 @@ describe('DeckSetupTools', () => { expect(vi.mocked(createModule)).toHaveBeenCalled() }) it('should close and add waste chute and staging area when done is called', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: 'wasteChuteAndStagingArea', + selectedModuleModel: HEATERSHAKER_MODULE_V1, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) render(props) - fireEvent.click(screen.getByText('Waste chute and staging area')) + fireEvent.click(screen.getByText('Waste chute and staging area slot')) fireEvent.click(screen.getByText('Done')) expect(props.onCloseClick).toHaveBeenCalled() expect(vi.mocked(createDeckFixture)).toHaveBeenCalledTimes(2) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/HoveredItems.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/HoveredItems.test.tsx new file mode 100644 index 00000000000..d07edb6d23c --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/HoveredItems.test.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { + FLEX_ROBOT_TYPE, + TEMPERATURE_MODULE_V2, + getDeckDefFromRobotType, +} from '@opentrons/shared-data' +import { LabwareRender, Module } from '@opentrons/components' +import { selectors } from '../../../../labware-ingred/selectors' +import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' +import { FixtureRender } from '../FixtureRender' +import { HoveredItems } from '../HoveredItems' +import type * as OpentronsComponents from '@opentrons/components' + +vi.mock('../FixtureRender') +vi.mock('../../../../labware-ingred/selectors') +vi.mock('../../../../labware-defs/selectors') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + LabwareRender: vi.fn(), + Module: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('HoveredItems', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + hoveredSlotPosition: [0, 0, 0], + deckDef: getDeckDefFromRobotType(FLEX_ROBOT_TYPE), + robotType: FLEX_ROBOT_TYPE, + hoveredLabware: null, + hoveredModule: null, + hoveredFixture: 'trashBin', + } + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: null, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({}) + vi.mocked(FixtureRender).mockReturnValue(
mock FixtureRender
) + vi.mocked(LabwareRender).mockReturnValue(
mock LabwareRender
) + vi.mocked(Module).mockReturnValue(
mock Module
) + }) + it('renders a hovered fixture', () => { + render(props) + screen.getByText('mock FixtureRender') + }) + it('renders a hovered labware', () => { + props.hoveredFixture = null + props.hoveredLabware = 'fixture/fixture_universal_flat_bottom_adapter/1' + render(props) + screen.getByText('mock LabwareRender') + screen.getByText('Fixture Opentrons Universal Flat Heater-Shaker Adapter') + }) + it('renders a hovered module', () => { + props.hoveredFixture = null + props.hoveredModule = TEMPERATURE_MODULE_V2 + render(props) + screen.getByText('mock Module') + screen.getByText('Temperature Module GEN2') + }) +}) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx index 96c12f731ff..a1f1127e5e6 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx @@ -4,6 +4,7 @@ import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' import { FLEX_ROBOT_TYPE, + THERMOCYCLER_MODULE_V1, fixtureP1000SingleV2Specs, fixtureTiprack1000ul, } from '@opentrons/shared-data' @@ -15,9 +16,14 @@ import { getPipetteEntities, } from '../../../../step-forms/selectors' import { getHas96Channel } from '../../../../utils' +import { selectors } from '../../../../labware-ingred/selectors' import { createCustomLabwareDef } from '../../../../labware-defs/actions' import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' import { getRobotType } from '../../../../file-data/selectors' +import { + selectLabware, + selectNestedLabware, +} from '../../../../labware-ingred/actions' import { LabwareTools } from '../LabwareTools' import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data' @@ -26,6 +32,8 @@ vi.mock('../../../../step-forms/selectors') vi.mock('../../../../file-data/selectors') vi.mock('../../../../labware-defs/selectors') vi.mock('../../../../labware-defs/actions') +vi.mock('../../../../labware-ingred/selectors') +vi.mock('../../../../labware-ingred/actions') const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -39,11 +47,7 @@ describe('LabwareTools', () => { beforeEach(() => { props = { slot: 'D3', - selectedHardware: null, - setSelectedLabwareDefURI: vi.fn(), - selecteLabwareDefURI: null, - setNestedSelectedLabwareDefURI: vi.fn(), - selectedNestedSelectedLabwareDefURI: null, + setHoveredLabware: vi.fn(), } vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({}) vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) @@ -57,6 +61,13 @@ describe('LabwareTools', () => { tiprackLabwareDef: [fixtureTiprack1000ul as LabwareDefinition2], }, }) + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: null, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) vi.mocked(getHas96Channel).mockReturnValue(false) vi.mocked(getInitialDeckSetup).mockReturnValue({ modules: {}, @@ -80,11 +91,16 @@ describe('LabwareTools', () => { screen.getByRole('label', { name: 'Corning 384 Well Plate' }) ) // set labware - expect(props.setSelectedLabwareDefURI).toHaveBeenCalled() + expect(vi.mocked(selectLabware)).toHaveBeenCalled() }) it('renders deck slot and selects an adapter and labware', () => { - props.selecteLabwareDefURI = - 'fixture/fixture_universal_flat_bottom_adapter/1' + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: 'fixture/fixture_universal_flat_bottom_adapter/1', + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: null, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) render(props) screen.getByText('Adapter') fireEvent.click(screen.getAllByTestId('ListButton_noActive')[4]) @@ -102,13 +118,26 @@ describe('LabwareTools', () => { name: 'Fixture Corning 96 Well Plate 360 µL Flat', }) ) - expect(props.setNestedSelectedLabwareDefURI).toHaveBeenCalled() + expect(vi.mocked(selectNestedLabware)).toHaveBeenCalled() }) it('renders the custom labware flow', () => { render(props) - screen.getByText('Add custom labware') + screen.getByText('Upload custom labware') fireEvent.change(screen.getByTestId('customLabwareInput')) expect(vi.mocked(createCustomLabwareDef)).toHaveBeenCalled() }) + + it('renders the filter checkbox if there is a module on the slot and is checked by default', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: THERMOCYCLER_MODULE_V1, + selectedSlot: { slot: 'B1', cutout: 'cutoutB1' }, + }) + render(props) + screen.getByText('Only display recommended labware') + expect(screen.getByRole('checkbox')).toBeChecked() + }) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx new file mode 100644 index 00000000000..c19a11918b8 --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx @@ -0,0 +1,127 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { + FLEX_ROBOT_TYPE, + HEATERSHAKER_MODULE_V1, + getDeckDefFromRobotType, +} from '@opentrons/shared-data' +import { LabwareRender, Module } from '@opentrons/components' +import { selectors } from '../../../../labware-ingred/selectors' +import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' +import { FixtureRender } from '../FixtureRender' +import { SelectedHoveredItems } from '../SelectedHoveredItems' +import type * as OpentronsComponents from '@opentrons/components' + +vi.mock('../FixtureRender') +vi.mock('../../../../labware-ingred/selectors') +vi.mock('../../../../labware-defs/selectors') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + LabwareRender: vi.fn(), + Module: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('SelectedHoveredItems', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + deckDef: getDeckDefFromRobotType(FLEX_ROBOT_TYPE), + robotType: FLEX_ROBOT_TYPE, + hoveredLabware: null, + hoveredModule: null, + hoveredFixture: null, + slotPosition: [0, 0, 0], + } + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: 'trashBin', + selectedModuleModel: null, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({}) + vi.mocked(FixtureRender).mockReturnValue(
mock FixtureRender
) + vi.mocked(LabwareRender).mockReturnValue(
mock LabwareRender
) + vi.mocked(Module).mockReturnValue(
mock Module
) + }) + it('renders a selected fixture by itself', () => { + render(props) + screen.getByText('mock FixtureRender') + expect(screen.queryByText('mock Module')).not.toBeInTheDocument() + }) + it('renders a selected fixture with a selected labware', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: 'fixture/fixture_universal_flat_bottom_adapter/1', + selectedNestedLabwareDefUri: null, + selectedFixture: 'trashBin', + selectedModuleModel: null, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + render(props) + screen.getByText('mock FixtureRender') + screen.getByText('mock LabwareRender') + expect(screen.queryByText('mock Module')).not.toBeInTheDocument() + screen.getByText('Fixture Opentrons Universal Flat Heater-Shaker Adapter') + }) + it('renders a selected module', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: HEATERSHAKER_MODULE_V1, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + render(props) + screen.getByText('mock Module') + expect(screen.queryByText('mock FixtureRender')).not.toBeInTheDocument() + screen.getByText('Heater-Shaker Module GEN1') + }) + it('renders a selected module and a selected labware', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: 'fixture/fixture_universal_flat_bottom_adapter/1', + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: HEATERSHAKER_MODULE_V1, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + render(props) + screen.getByText('mock Module') + expect(screen.queryByText('mock FixtureRender')).not.toBeInTheDocument() + screen.getByText('Heater-Shaker Module GEN1') + screen.getByText('Fixture Opentrons Universal Flat Heater-Shaker Adapter') + }) + it('renders selected fixture and both labware and nested labware', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: 'fixture/fixture_universal_flat_bottom_adapter/1', + selectedNestedLabwareDefUri: + 'fixture/fixture_universal_flat_bottom_adapter/1', + selectedFixture: 'trashBin', + selectedModuleModel: null, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + render(props) + screen.getByText('mock FixtureRender') + expect(screen.getAllByText('mock LabwareRender')).toHaveLength(2) + expect( + screen.getAllByText( + 'Fixture Opentrons Universal Flat Heater-Shaker Adapter' + ) + ).toHaveLength(2) + }) + it('renders nothing when there is a hovered module but selected fixture', () => { + props.hoveredModule = HEATERSHAKER_MODULE_V1 + render(props) + expect(screen.queryByText('mock FixtureRender')).not.toBeInTheDocument() + }) +}) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx new file mode 100644 index 00000000000..ad2841337f8 --- /dev/null +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx @@ -0,0 +1,117 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { fireEvent, screen } from '@testing-library/react' +import { fixture96Plate } from '@opentrons/shared-data' +import { i18n } from '../../../../assets/localization' +import { renderWithProviders } from '../../../../__testing-utils__' +import { + deleteContainer, + duplicateLabware, + openIngredientSelector, +} from '../../../../labware-ingred/actions' +import { EditNickNameModal } from '../../../../organisms' +import { deleteModule } from '../../../../step-forms/actions' +import { deleteDeckFixture } from '../../../../step-forms/actions/additionalItems' +import { getDeckSetupForActiveItem } from '../../../../top-selectors/labware-locations' +import { SlotOverflowMenu } from '../SlotOverflowMenu' + +import type { NavigateFunction } from 'react-router-dom' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +const mockNavigate = vi.fn() + +vi.mock('../../../../top-selectors/labware-locations') +vi.mock('../../../../step-forms/actions') +vi.mock('../../../../labware-ingred/actions') +vi.mock('../../../../step-forms/actions/additionalItems') +vi.mock('../../../../organisms') +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useNavigate: () => mockNavigate, + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('SlotOverflowMenu', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + location: 'D3', + setShowMenuList: vi.fn(), + addEquipment: vi.fn(), + } + + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + labware: { + labId: { + slot: 'D3', + id: 'labId', + labwareDefURI: 'mockUri', + def: fixture96Plate as LabwareDefinition2, + }, + lab2: { + slot: 'labId', + id: 'labId2', + labwareDefURI: 'mockUri', + def: fixture96Plate as LabwareDefinition2, + }, + }, + pipettes: {}, + modules: { + mod: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + id: 'modId', + slot: 'D3', + moduleState: {} as any, + }, + }, + additionalEquipmentOnDeck: { + fixture: { name: 'stagingArea', id: 'mockId', location: 'cutoutD3' }, + }, + }) + vi.mocked(EditNickNameModal).mockReturnValue( +
mockEditNickNameModal
+ ) + }) + it('should renders all buttons as enabled and clicking on them calls ctas', () => { + render(props) + fireEvent.click( + screen.getByRole('button', { name: 'Edit hardware/labware' }) + ) + expect(props.addEquipment).toHaveBeenCalled() + expect(props.setShowMenuList).toHaveBeenCalled() + fireEvent.click(screen.getByRole('button', { name: 'Rename labware' })) + screen.getByText('mockEditNickNameModal') + fireEvent.click(screen.getByRole('button', { name: 'Add liquid' })) + expect(mockNavigate).toHaveBeenCalled() + expect(vi.mocked(openIngredientSelector)).toHaveBeenCalled() + fireEvent.click(screen.getByRole('button', { name: 'Duplicate labware' })) + expect(vi.mocked(duplicateLabware)).toHaveBeenCalled() + expect(props.setShowMenuList).toHaveBeenCalled() + fireEvent.click(screen.getByRole('button', { name: 'Clear slot' })) + expect(vi.mocked(deleteContainer)).toHaveBeenCalledTimes(2) + expect(vi.mocked(deleteModule)).toHaveBeenCalled() + expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalled() + expect(props.setShowMenuList).toHaveBeenCalled() + }) + it('renders 2 buttons when there is nothing on the slot', () => { + props.location = 'A1' + render(props) + fireEvent.click( + screen.getByRole('button', { name: 'Add hardware/labware' }) + ) + expect(props.addEquipment).toHaveBeenCalled() + expect(props.setShowMenuList).toHaveBeenCalled() + expect(screen.getAllByRole('button')).toHaveLength(2) + }) +}) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts index 86f01459653..d990339c58b 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts @@ -1,12 +1,16 @@ import { describe, it, expect } from 'vitest' import { FLEX_ROBOT_TYPE, + HEATERSHAKER_MODULE_TYPE, + HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_V1, + MAGNETIC_MODULE_TYPE, + MAGNETIC_MODULE_V1, OT2_ROBOT_TYPE, THERMOCYCLER_MODULE_V1, THERMOCYCLER_MODULE_V2, } from '@opentrons/shared-data' -import { getModuleModelsBySlot } from '../utils' +import { getModuleModelsBySlot, getDeckErrors } from '../utils' import { FLEX_MODULE_MODELS, OT2_MODULE_MODELS } from '../constants' describe('getModuleModelsBySlot', () => { @@ -25,6 +29,15 @@ describe('getModuleModelsBySlot', () => { ) expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '1')).toEqual(noTC) }) + it('renders ot-2 modules minus thermocyclers & heater-shaker for slot 9', () => { + const noTCAndHS = OT2_MODULE_MODELS.filter( + model => + model !== THERMOCYCLER_MODULE_V1 && + model !== THERMOCYCLER_MODULE_V2 && + model !== HEATERSHAKER_MODULE_V1 + ) + expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '9')).toEqual(noTCAndHS) + }) it('renders flex modules for middle slots', () => { expect(getModuleModelsBySlot(false, FLEX_ROBOT_TYPE, 'B2')).toEqual([ MAGNETIC_BLOCK_V1, @@ -42,3 +55,55 @@ describe('getModuleModelsBySlot', () => { expect(getModuleModelsBySlot(false, FLEX_ROBOT_TYPE, 'C1')).toEqual(noTC) }) }) + +describe('getDeckErrors', () => { + it('renders no error when there is no conflict', () => { + expect( + getDeckErrors({ + modules: {}, + selectedSlot: '1', + selectedModel: MAGNETIC_MODULE_V1, + labware: {}, + robotType: OT2_ROBOT_TYPE, + }) + ).toEqual(null) + }) + it('renders H-S adjacent error', () => { + expect( + getDeckErrors({ + modules: { + hs: { + model: HEATERSHAKER_MODULE_V1, + type: HEATERSHAKER_MODULE_TYPE, + id: 'mockId', + slot: '4', + moduleState: {} as any, + }, + }, + selectedSlot: '1', + selectedModel: MAGNETIC_MODULE_V1, + labware: {}, + robotType: OT2_ROBOT_TYPE, + }) + ).toEqual('heater_shaker_adjacent') + }) + it('renders module adjacent error', () => { + expect( + getDeckErrors({ + modules: { + hs: { + model: MAGNETIC_MODULE_V1, + type: MAGNETIC_MODULE_TYPE, + id: 'mockId', + slot: '4', + moduleState: {} as any, + }, + }, + selectedSlot: '1', + selectedModel: HEATERSHAKER_MODULE_V1, + labware: {}, + robotType: OT2_ROBOT_TYPE, + }) + ).toEqual('heater_shaker_adjacent_to') + }) +}) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts index 7ce6f66b5c5..53571367f8b 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts @@ -90,7 +90,9 @@ export const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = { 'nest_96_wellplate_2ml_deep', 'opentrons_96_wellplate_200ul_pcr_full_skirt', ], - [ABSORBANCE_READER_TYPE]: [], + [ABSORBANCE_READER_TYPE]: [ + 'opentrons_flex_lid_absorbance_plate_reader_module', + ], } export const MOAM_MODELS_WITH_FF: ModuleModel[] = [TEMPERATURE_MODULE_V2] diff --git a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts index e3c409e1f12..8109b8ca50e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts @@ -1,12 +1,16 @@ +import some from 'lodash/some' import { FLEX_ROBOT_TYPE, FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, + HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_V1, OT2_ROBOT_TYPE, THERMOCYCLER_MODULE_TYPE, THERMOCYCLER_MODULE_V2, + getAreSlotsAdjacent, getModuleType, } from '@opentrons/shared-data' + import { getOnlyLatestDefs } from '../../../labware-defs' import { FLEX_MODULE_MODELS, @@ -18,11 +22,16 @@ import type { AddressableAreaName, CutoutFixture, CutoutId, + DeckDefinition, DeckSlotId, LabwareDefinition2, ModuleModel, RobotType, } from '@opentrons/shared-data' +import type { InitialDeckSetup } from '../../../step-forms' + +const OT2_TC_SLOTS = ['7', '8', '10', '11'] +const FLEX_TC_SLOTS = ['A1', 'B1'] export function getCutoutIdForAddressableArea( addressableArea: AddressableAreaName, @@ -75,12 +84,18 @@ export function getModuleModelsBySlot( case OT2_ROBOT_TYPE: { if (OT2_MIDDLE_SLOTS.includes(slot)) { moduleModels = [] - } else if (slot !== '7') { + } else if (slot === '7') { + moduleModels = OT2_MODULE_MODELS + } else if (slot === '9') { moduleModels = OT2_MODULE_MODELS.filter( - model => getModuleType(model) !== THERMOCYCLER_MODULE_TYPE + model => + getModuleType(model) !== HEATERSHAKER_MODULE_TYPE && + getModuleType(model) !== THERMOCYCLER_MODULE_TYPE ) } else { - moduleModels = OT2_MODULE_MODELS + moduleModels = OT2_MODULE_MODELS.filter( + model => getModuleType(model) !== THERMOCYCLER_MODULE_TYPE + ) } break } @@ -124,3 +139,117 @@ export const getLabwareCompatibleWithAdapter = ( ) .map(([labwareDefUri]) => labwareDefUri) } + +interface DeckErrorsProps { + modules: InitialDeckSetup['modules'] + selectedSlot: string + selectedModel: ModuleModel + labware: InitialDeckSetup['labware'] + robotType: RobotType +} + +export const getDeckErrors = (props: DeckErrorsProps): string | null => { + const { selectedSlot, selectedModel, modules, labware, robotType } = props + + let error = null + + if (robotType === OT2_ROBOT_TYPE) { + const isModuleAdjacentToHeaterShaker = + // modules can't be adjacent to heater shakers + getModuleType(selectedModel) !== HEATERSHAKER_MODULE_TYPE && + some( + modules, + hwModule => + hwModule.type === HEATERSHAKER_MODULE_TYPE && + getAreSlotsAdjacent(hwModule.slot, selectedSlot) + ) + + if (isModuleAdjacentToHeaterShaker) { + error = 'heater_shaker_adjacent' + } else if (getModuleType(selectedModel) === HEATERSHAKER_MODULE_TYPE) { + const isHeaterShakerAdjacentToAnotherModule = some( + modules, + hwModule => + getAreSlotsAdjacent(hwModule.slot, selectedSlot) && + // if the module is a heater shaker, it can't be adjacent to another module + hwModule.type !== HEATERSHAKER_MODULE_TYPE + ) + if (isHeaterShakerAdjacentToAnotherModule) { + error = 'heater_shaker_adjacent_to' + } + } else if (getModuleType(selectedModel) === THERMOCYCLER_MODULE_TYPE) { + const isLabwareInTCSlots = Object.values(labware).some(lw => + OT2_TC_SLOTS.includes(lw.slot) + ) + if (isLabwareInTCSlots) { + error = 'tc_slots_occupied_ot2' + } + } + } else { + if (getModuleType(selectedModel) === THERMOCYCLER_MODULE_TYPE) { + const isLabwareInTCSlots = Object.values(labware).some(lw => + FLEX_TC_SLOTS.includes(lw.slot) + ) + if (isLabwareInTCSlots) { + error = 'tc_slots_occupied_flex' + } + } + } + + return error +} + +interface ZoomInOnCoordinateProps { + x: number + y: number + deckDef: DeckDefinition +} +export function zoomInOnCoordinate(props: ZoomInOnCoordinateProps): string { + const { x, y, deckDef } = props + const [width, height] = [deckDef.dimensions[0], deckDef.dimensions[1]] + + const zoomFactor = 0.6 + const newWidth = width * zoomFactor + const newHeight = height * zoomFactor + + // +125 and +50 to get the approximate center of the screen point + const newMinX = x - newWidth / 2 + 125 + const newMinY = y - newHeight / 2 + 50 + + return `${newMinX} ${newMinY} ${newWidth} ${newHeight}` +} + +export interface AnimateZoomProps { + targetViewBox: string + viewBox: string + setViewBox: React.Dispatch> +} + +type ViewBox = [number, number, number, number] + +export function animateZoom(props: AnimateZoomProps): void { + const { targetViewBox, viewBox, setViewBox } = props + + if (targetViewBox === viewBox) return + + const duration = 500 + const start = performance.now() + const initialViewBoxValues = viewBox.split(' ').map(Number) as ViewBox + const targetViewBoxValues = targetViewBox.split(' ').map(Number) as ViewBox + + const animate = (time: number): void => { + const elapsed = time - start + const progress = Math.min(elapsed / duration, 1) + + const interpolatedViewBox = initialViewBoxValues.map( + (start, index) => start + progress * (targetViewBoxValues[index] - start) + ) + + setViewBox(interpolatedViewBox.join(' ')) + + if (progress < 1) { + requestAnimationFrame(animate) + } + } + requestAnimationFrame(animate) +} diff --git a/protocol-designer/src/pages/Designer/LabwareLabel.tsx b/protocol-designer/src/pages/Designer/LabwareLabel.tsx new file mode 100644 index 00000000000..feed9a2c43a --- /dev/null +++ b/protocol-designer/src/pages/Designer/LabwareLabel.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import { DeckLabelSet } from '@opentrons/components' +import type { DeckLabelProps } from '@opentrons/components' +import type { + CoordinateTuple, + LabwareDefinition2, +} from '@opentrons/shared-data' + +interface ModuleLabelProps { + position: CoordinateTuple + labwareDef: LabwareDefinition2 + isSelected: boolean + isLast: boolean + nestedLabwareInfo?: DeckLabelProps[] +} +export const LabwareLabel = (props: ModuleLabelProps): JSX.Element => { + const { + labwareDef, + position, + isSelected, + isLast, + nestedLabwareInfo = [], + } = props + const labelContainerRef = React.useRef(null) + const [labelContainerHeight, setLabelContainerHeight] = React.useState(0) + + const deckLabels = [ + ...nestedLabwareInfo, + { + text: labwareDef.metadata.displayName, + isSelected: isSelected, + isLast: isLast, + }, + ] + + React.useEffect(() => { + if (labelContainerRef.current) { + setLabelContainerHeight(labelContainerRef.current.offsetHeight) + } + }, [nestedLabwareInfo]) + + return ( + + ) +} diff --git a/protocol-designer/src/pages/Designer/LiquidsOverflowMenu.tsx b/protocol-designer/src/pages/Designer/LiquidsOverflowMenu.tsx new file mode 100644 index 00000000000..6e36a3ccb19 --- /dev/null +++ b/protocol-designer/src/pages/Designer/LiquidsOverflowMenu.tsx @@ -0,0 +1,111 @@ +import * as React from 'react' +import styled from 'styled-components' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { useLocation } from 'react-router-dom' +import { + ALIGN_CENTER, + BORDERS, + Box, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + LiquidIcon, + NO_WRAP, + POSITION_ABSOLUTE, + SPACING, + StyledText, +} from '@opentrons/components' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import * as labwareIngredActions from '../../labware-ingred/actions' +import type { ThunkDispatch } from '../../types' + +const NAV_HEIGHT = '64px' + +interface LiquidsOverflowMenuProps { + onClose: () => void + showLiquidsModal: () => void + overflowWrapperRef: React.RefObject +} + +export function LiquidsOverflowMenu( + props: LiquidsOverflowMenuProps +): JSX.Element { + const { onClose, showLiquidsModal, overflowWrapperRef } = props + const location = useLocation() + const { t } = useTranslation(['starting_deck_state']) + const liquids = useSelector(labwareIngredSelectors.allIngredientNamesIds) + const dispatch: ThunkDispatch = useDispatch() + + return ( + { + e.preventDefault() + e.stopPropagation() + }} + > + {liquids.map(({ name, displayColor, ingredientId }) => { + return ( + { + onClose() + showLiquidsModal() + dispatch(labwareIngredActions.selectLiquidGroup(ingredientId)) + }} + key={ingredientId} + > + + + {name} + + + ) + })} + {liquids.length > 0 ? ( + + ) : null} + { + onClose() + showLiquidsModal() + dispatch(labwareIngredActions.createNewLiquidGroup()) + }} + key="defineLiquid" + > + + + + {t('define_liquid')} + + + + + ) +} +const MenuButton = styled.button` + background-color: ${COLORS.transparent}; + cursor: pointer; + padding: ${SPACING.spacing8} ${SPACING.spacing12}; + border: none; + border-radius: inherit; + &:hover { + background-color: ${COLORS.blue10}; + } + &:disabled { + color: ${COLORS.grey40}; + cursor: auto; + } +` diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx new file mode 100644 index 00000000000..9dbddefbfba --- /dev/null +++ b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx @@ -0,0 +1,164 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + EmptySelectorButton, + Flex, + JUSTIFY_CENTER, + LabwareRender, + OVERFLOW_SCROLL, + RobotWorkSpace, + SPACING, + StyledText, + WRAP, +} from '@opentrons/components' +import * as wellContentsSelectors from '../../../top-selectors/well-contents' +import { wellFillFromWellContents } from '../../../components/labware' +import { selectors } from '../../../labware-ingred/selectors' +import { START_TERMINAL_ITEM_ID } from '../../../steplist' +import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' +import { DeckItemHover } from '../DeckSetup/DeckItemHover' +import { SlotDetailsContainer } from '../../../organisms' +import { getRobotType } from '../../../file-data/selectors' +import { SlotOverflowMenu } from '../DeckSetup/SlotOverflowMenu' +import type { DeckSlotId } from '@opentrons/shared-data' + +interface OffDeckDetailsProps { + addLabware: () => void +} +export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { + const { addLabware } = props + const { t, i18n } = useTranslation('starting_deck_state') + const [hoverSlot, setHoverSlot] = React.useState(null) + const [menuListId, setShowMenuListForId] = React.useState( + null + ) + const robotType = useSelector(getRobotType) + const deckSetup = useSelector(getDeckSetupForActiveItem) + const offDeckLabware = Object.values(deckSetup.labware).filter( + lw => lw.slot === 'offDeck' + ) + const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors) + const allWellContentsForActiveItem = useSelector( + wellContentsSelectors.getAllWellContentsForActiveItem + ) + + return ( + + {hoverSlot != null ? ( + + + + ) : null} + + + + {i18n.format(t('off_deck_labware'), 'upperCase')} + + + + + {offDeckLabware.map(lw => { + const wellContents = allWellContentsForActiveItem + ? allWellContentsForActiveItem[lw.id] + : null + const definition = lw.def + const { dimensions } = definition + const xyzDimensions = { + xDimension: dimensions.xDimension ?? 0, + yDimension: dimensions.yDimension ?? 0, + zDimension: dimensions.zDimension ?? 0, + } + return ( + + + {() => ( + <> + + + + )} + + {menuListId === lw.id ? ( + // TODO fix this rendering position + + { + setShowMenuListForId(null) + }} + /> + + ) : null} + + ) + })} + + + + + + + ) +} diff --git a/protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx b/protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx new file mode 100644 index 00000000000..f8dad4ba4ea --- /dev/null +++ b/protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx @@ -0,0 +1,169 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { + ALIGN_CENTER, + BORDERS, + Box, + COLORS, + DIRECTION_COLUMN, + Flex, + JUSTIFY_CENTER, + LabwareRender, + RobotCoordsForeignDiv, + RobotWorkSpace, + SPACING, + StyledText, +} from '@opentrons/components' +import { getCustomLabwareDefsByURI } from '../../../labware-defs/selectors' +import { getOnlyLatestDefs } from '../../../labware-defs' +import { selectors } from '../../../labware-ingred/selectors' +import { selectZoomedIntoSlot } from '../../../labware-ingred/actions' +import { DeckSetupTools } from '../DeckSetup/DeckSetupTools' +import { LabwareLabel } from '../LabwareLabel' +import { OffDeckDetails } from './OffDeckDetails' + +const STANDARD_X_WIDTH = '127.76px' +const STANDARD_Y_HEIGHT = '85.48px' + +export function OffDeck(): JSX.Element { + const { t, i18n } = useTranslation('starting_deck_state') + const [hoveredLabware, setHoveredLabware] = React.useState( + null + ) + const dispatch = useDispatch() + + const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) + const { selectedLabwareDefUri, selectedSlot } = selectedSlotInfo + + const customLabwareDefs = useSelector(getCustomLabwareDefsByURI) + const defs = getOnlyLatestDefs() + + const hoveredLabwareDef = + hoveredLabware != null + ? defs[hoveredLabware] ?? customLabwareDefs[hoveredLabware] ?? null + : null + const offDeckLabware = + selectedLabwareDefUri != null ? defs[selectedLabwareDefUri] ?? null : null + + let labware = ( + + {() => ( + + + + )} + + ) + if (hoveredLabwareDef != null && hoveredLabwareDef !== offDeckLabware) { + labware = ( + + {() => ( + <> + + + + )} + + ) + } else if (offDeckLabware != null) { + const def = offDeckLabware + labware = ( + + {() => ( + <> + + + + + )} + + ) + } + + return ( + <> + {selectedSlot.slot === 'offDeck' ? ( + <> + + + + + + {i18n.format(t('off_deck_labware'), 'upperCase')} + + + + {labware} + + + + + { + dispatch(selectZoomedIntoSlot({ slot: null, cutout: null })) + }} + /> + + ) : ( + { + dispatch(selectZoomedIntoSlot({ slot: 'offDeck', cutout: null })) + }} + /> + )} + + ) +} diff --git a/protocol-designer/src/pages/Designer/Offdeck/__tests__/OffDeckDetails.test.tsx b/protocol-designer/src/pages/Designer/Offdeck/__tests__/OffDeckDetails.test.tsx new file mode 100644 index 00000000000..1e5f932e17c --- /dev/null +++ b/protocol-designer/src/pages/Designer/Offdeck/__tests__/OffDeckDetails.test.tsx @@ -0,0 +1,65 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { FLEX_ROBOT_TYPE, fixture12Trough } from '@opentrons/shared-data' +import { screen } from '@testing-library/react' +import { i18n } from '../../../../assets/localization' +import { renderWithProviders } from '../../../../__testing-utils__' +import { selectors } from '../../../../labware-ingred/selectors' +import { getRobotType } from '../../../../file-data/selectors' +import { getDeckSetupForActiveItem } from '../../../../top-selectors/labware-locations' +import { getAllWellContentsForActiveItem } from '../../../../top-selectors/well-contents' +import { OffDeckDetails } from '../OffDeckDetails' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type * as Components from '@opentrons/components' + +vi.mock('../../../../top-selectors/labware-locations') +vi.mock('../../../../file-data/selectors') +vi.mock('../../../../labware-ingred/selectors') +vi.mock('../../../../top-selectors/well-contents') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + LabwareRender: () =>
mock LabwareRender
, + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('OffDeckDetails', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + addLabware: vi.fn(), + } + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + modules: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + labware: { + labware: { + id: 'mockId', + def: fixture12Trough as LabwareDefinition2, + labwareDefURI: 'mockDefUri', + slot: 'offDeck', + }, + }, + }) + vi.mocked(selectors.getLiquidDisplayColors).mockReturnValue([]) + vi.mocked(getAllWellContentsForActiveItem).mockReturnValue({}) + }) + + it('renders off-deck overview with 1 labware', () => { + render(props) + screen.getByText('OFF-DECK LABWARE') + screen.getByText('mock LabwareRender') + screen.getByText('Add labware') + }) +}) diff --git a/protocol-designer/src/pages/Designer/Offdeck/__tests__/Offdeck.test.tsx b/protocol-designer/src/pages/Designer/Offdeck/__tests__/Offdeck.test.tsx new file mode 100644 index 00000000000..167cf7437ae --- /dev/null +++ b/protocol-designer/src/pages/Designer/Offdeck/__tests__/Offdeck.test.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { screen } from '@testing-library/react' +import { selectors } from '../../../../labware-ingred/selectors' +import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' +import { renderWithProviders } from '../../../../__testing-utils__' +import { OffDeckDetails } from '../OffDeckDetails' +import { OffDeck } from '../Offdeck' +import type * as Components from '@opentrons/components' + +vi.mock('../OffDeckDetails') +vi.mock('../../../../labware-ingred/selectors') +vi.mock('../../../../labware-defs/selectors') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + LabwareRender: () =>
mock LabwareRender
, + } +}) + +const render = () => { + return renderWithProviders()[0] +} + +describe('OffDeck', () => { + beforeEach(() => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: null, + selectedSlot: { slot: null, cutout: null }, + }) + vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({}) + }) + it('renders off deck details', () => { + vi.mocked(OffDeckDetails).mockReturnValue(
mock off deck details
) + render() + screen.getByText('mock off deck details') + }) +}) diff --git a/protocol-designer/src/pages/Designer/Offdeck/index.ts b/protocol-designer/src/pages/Designer/Offdeck/index.ts new file mode 100644 index 00000000000..0ca8caf1f01 --- /dev/null +++ b/protocol-designer/src/pages/Designer/Offdeck/index.ts @@ -0,0 +1 @@ +export * from './Offdeck' diff --git a/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx b/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx index 0aab3dabba3..97ac029d447 100644 --- a/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx +++ b/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx @@ -1,3 +1,105 @@ -import { it } from 'vitest' +import * as React from 'react' -it.todo('write test for Designer') +import { describe, it, vi, beforeEach, expect } from 'vitest' +import { MemoryRouter } from 'react-router-dom' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' +import { selectors } from '../../../labware-ingred/selectors' +import { getFileMetadata } from '../../../file-data/selectors' +import { generateNewProtocol } from '../../../labware-ingred/actions' +import { DeckSetupContainer } from '../DeckSetup' +import { Designer } from '../index' +import { LiquidsOverflowMenu } from '../LiquidsOverflowMenu' + +import type { NavigateFunction } from 'react-router-dom' + +const mockNavigate = vi.fn() + +vi.mock('../../../labware-ingred/actions') +vi.mock('../../../labware-ingred/selectors') +vi.mock('../LiquidsOverflowMenu') +vi.mock('../DeckSetup') +vi.mock('../../../file-data/selectors') +vi.mock('../../../top-selectors/labware-locations') +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useNavigate: () => mockNavigate, + } +}) + +const render = () => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + )[0] +} + +describe('Designer', () => { + beforeEach(() => { + vi.mocked(getFileMetadata).mockReturnValue({ + protocolName: 'mockProtocolName', + created: 123, + }) + vi.mocked(selectors.getIsNewProtocol).mockReturnValue(true) + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + modules: {}, + additionalEquipmentOnDeck: { + trash: { name: 'trashBin', location: 'cutoutA3', id: 'mockId' }, + }, + labware: {}, + pipettes: {}, + }) + vi.mocked(DeckSetupContainer).mockReturnValue( +
mock DeckSetupContainer
+ ) + vi.mocked(LiquidsOverflowMenu).mockReturnValue( +
mock LiquidsOverflowMenu
+ ) + vi.mocked(selectors.getZoomedInSlot).mockReturnValue({ + slot: null, + cutout: null, + }) + }) + + it('renders deck setup container and nav buttons', () => { + render() + screen.getByText('mock DeckSetupContainer') + screen.getByText('mockProtocolName') + screen.getByText('Edit protocol') + screen.getByText('Protocol steps') + screen.getByText('Protocol starting deck') + screen.getByText('Liquids') + fireEvent.click(screen.getByRole('button', { name: 'Done' })) + expect(mockNavigate).toHaveBeenCalledWith('/overview') + }) + + it('renders the liquids button overflow menu', () => { + render() + fireEvent.click(screen.getByText('Liquids')) + screen.getByText('mock LiquidsOverflowMenu') + }) + + it('calls generateNewProtocol when hardware has been placed for a new protocol', () => { + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ + modules: {}, + additionalEquipmentOnDeck: { + wasteChute: { name: 'wasteChute', id: 'mockId', location: 'cutoutD3' }, + trashBin: { name: 'trashBin', id: 'mockId', location: 'cutoutA3' }, + }, + labware: {}, + pipettes: {}, + }) + render() + expect(vi.mocked(generateNewProtocol)).toHaveBeenCalled() + }) + + it.todo('renders the protocol steps page') +}) diff --git a/protocol-designer/src/pages/Designer/__tests__/LiquidsOverflowMenu.test.tsx b/protocol-designer/src/pages/Designer/__tests__/LiquidsOverflowMenu.test.tsx new file mode 100644 index 00000000000..24877f61294 --- /dev/null +++ b/protocol-designer/src/pages/Designer/__tests__/LiquidsOverflowMenu.test.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors' +import * as labwareIngredActions from '../../../labware-ingred/actions' +import { renderWithProviders } from '../../../__testing-utils__' +import { LiquidsOverflowMenu } from '../LiquidsOverflowMenu' + +import type { NavigateFunction } from 'react-router-dom' + +const mockLocation = vi.fn() + +vi.mock('../../../labware-ingred/selectors') +vi.mock('../../../labware-ingred/actions') +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useLocation: () => mockLocation, + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('SlotOverflowMenu', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onClose: vi.fn(), + showLiquidsModal: vi.fn(), + overflowWrapperRef: React.createRef(), + } + vi.mocked(labwareIngredSelectors.allIngredientNamesIds).mockReturnValue([ + { + displayColor: 'mockColor', + name: 'mockname', + ingredientId: '0', + }, + ]) + }) + it('renders the overflow buttons with 1 liquid defined', () => { + render(props) + screen.getByText('mockname') + fireEvent.click(screen.getByTestId('mockname_0')) + expect(props.onClose).toHaveBeenCalled() + expect(props.showLiquidsModal).toHaveBeenCalled() + expect(vi.mocked(labwareIngredActions.selectLiquidGroup)).toHaveBeenCalled() + screen.getByText('Define a liquid') + fireEvent.click(screen.getByTestId('defineLiquid')) + expect(props.onClose).toHaveBeenCalled() + expect( + vi.mocked(labwareIngredActions.createNewLiquidGroup) + ).toHaveBeenCalled() + expect(props.showLiquidsModal).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/pages/Designer/__tests__/utils.test.ts b/protocol-designer/src/pages/Designer/__tests__/utils.test.ts new file mode 100644 index 00000000000..b1faa3f0d67 --- /dev/null +++ b/protocol-designer/src/pages/Designer/__tests__/utils.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect } from 'vitest' +import { + HEATERSHAKER_MODULE_TYPE, + HEATERSHAKER_MODULE_V1, + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V1, + WASTE_CHUTE_CUTOUT, + fixture96Plate, +} from '@opentrons/shared-data' +import { getSlotInformation } from '../utils' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { AdditionalEquipmentName } from '@opentrons/step-generation' +import type { AllTemporalPropertiesForTimelineFrame } from '../../../step-forms' + +const mockLabOnDeck1 = { + slot: 'mockHsId', + id: 'labId', + labwareDefURI: 'mockUri', + def: fixture96Plate as LabwareDefinition2, +} +const mockLabOnDeck2 = { + slot: 'labId', + id: 'labId2', + labwareDefURI: 'mockUri2', + def: fixture96Plate as LabwareDefinition2, +} +const mockLabOnDeck3 = { + slot: '2', + id: 'labId3', + labwareDefURI: 'mockUri3', + def: fixture96Plate as LabwareDefinition2, +} +const mockHS = { + id: 'mockHsId', + model: HEATERSHAKER_MODULE_V1, + type: HEATERSHAKER_MODULE_TYPE, + slot: '1', + moduleState: {} as any, +} + +const mockOt2DeckSetup: AllTemporalPropertiesForTimelineFrame = { + labware: { + labId: mockLabOnDeck1, + lab2: mockLabOnDeck2, + lab3: mockLabOnDeck3, + }, + pipettes: {}, + modules: { + hs: mockHS, + temp: { + id: 'mockTempId', + model: TEMPERATURE_MODULE_V1, + type: TEMPERATURE_MODULE_TYPE, + slot: '3', + moduleState: {} as any, + }, + }, + additionalEquipmentOnDeck: { + trash: { name: 'trashBin', id: 'mockTrashId', location: '12' }, + }, +} + +const mockLabOnStagingArea = { + slot: 'D4', + id: 'labId3', + labwareDefURI: 'mockUri3', + def: fixture96Plate as LabwareDefinition2, +} +const mockHSFlex = { + id: 'mockHsId', + model: HEATERSHAKER_MODULE_V1, + type: HEATERSHAKER_MODULE_TYPE, + slot: 'D1', + moduleState: {} as any, +} +const mockTrash = { + name: 'trashBin' as AdditionalEquipmentName, + id: 'mockTrashId', + location: 'cutoutA3', +} +const mockWasteChute = { + name: 'wasteChute' as AdditionalEquipmentName, + id: 'mockWasteChuteId', + location: WASTE_CHUTE_CUTOUT, +} +const mockStagingArea = { + name: 'stagingArea' as AdditionalEquipmentName, + id: 'mockStagingAreaId', + location: WASTE_CHUTE_CUTOUT, +} +const mockFlex2DeckSetup: AllTemporalPropertiesForTimelineFrame = { + labware: { + labId: mockLabOnDeck1, + lab2: mockLabOnDeck2, + lab3: mockLabOnStagingArea, + }, + pipettes: {}, + modules: { + hs: mockHSFlex, + temp: { + id: 'mockTempId', + model: TEMPERATURE_MODULE_V1, + type: TEMPERATURE_MODULE_TYPE, + slot: 'C1', + moduleState: {} as any, + }, + }, + additionalEquipmentOnDeck: { + trash: mockTrash, + wasteChute: mockWasteChute, + stagingArea: mockStagingArea, + }, +} + +describe('getSlotInformation', () => { + it('renders a heater-shaker with a labware and nested labware for an ot-2 in slot 1 with other mods added', () => { + expect( + getSlotInformation({ deckSetup: mockOt2DeckSetup, slot: '1' }) + ).toEqual({ + createdModuleForSlot: mockHS, + createdLabwareForSlot: mockLabOnDeck1, + createdNestedLabwareForSlot: mockLabOnDeck2, + createFixtureForSlots: [], + slotPosition: null, + }) + }) + it('renders only a labware for ot-2 on slot 2', () => { + expect( + getSlotInformation({ deckSetup: mockOt2DeckSetup, slot: '2' }) + ).toEqual({ + createdLabwareForSlot: mockLabOnDeck3, + createFixtureForSlots: [], + slotPosition: null, + }) + }) + it('renders no items on the slot for a flex', () => { + const mockDeckSetup: AllTemporalPropertiesForTimelineFrame = { + labware: {}, + pipettes: {}, + modules: {}, + additionalEquipmentOnDeck: {}, + } + expect( + getSlotInformation({ deckSetup: mockDeckSetup, slot: 'A1' }) + ).toEqual({ slotPosition: null, createFixtureForSlots: [] }) + }) + it('renders a trashbin for a Flex on slot A3', () => { + expect( + getSlotInformation({ deckSetup: mockFlex2DeckSetup, slot: 'A3' }) + ).toEqual({ + slotPosition: null, + createFixtureForSlots: [mockTrash], + preSelectedFixture: 'trashBin', + }) + }) + it('renders a h-s, labware and nested labware for a Flex on slot D1', () => { + expect( + getSlotInformation({ deckSetup: mockFlex2DeckSetup, slot: 'D1' }) + ).toEqual({ + slotPosition: null, + createdModuleForSlot: mockHSFlex, + createdLabwareForSlot: mockLabOnDeck1, + createdNestedLabwareForSlot: mockLabOnDeck2, + createFixtureForSlots: [], + }) + }) + it('renders the waste chute and staging area for slot D3 for Flex', () => { + expect( + getSlotInformation({ deckSetup: mockFlex2DeckSetup, slot: 'D3' }) + ).toEqual({ + slotPosition: null, + createFixtureForSlots: [mockWasteChute, mockStagingArea], + preSelectedFixture: 'wasteChuteAndStagingArea', + }) + }) + it('renders the staging area with waste chute and labware in slot D4 for flex', () => { + expect( + getSlotInformation({ deckSetup: mockFlex2DeckSetup, slot: 'D4' }) + ).toEqual({ + slotPosition: null, + createdLabwareForSlot: mockLabOnStagingArea, + createFixtureForSlots: [mockWasteChute, mockStagingArea], + preSelectedFixture: 'wasteChuteAndStagingArea', + }) + }) +}) diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index 7c07db15646..689405e8eba 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -1,15 +1,78 @@ import * as React from 'react' - -import { Tabs } from '@opentrons/components' - +import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import { + ALIGN_CENTER, + ALIGN_END, + COLORS, + DIRECTION_COLUMN, + Flex, + INFO_TOAST, + Icon, + JUSTIFY_SPACE_BETWEEN, + PrimaryButton, + SPACING, + SecondaryButton, + StyledText, + Tabs, + ToggleGroup, + useOnClickOutside, +} from '@opentrons/components' +import { useKitchen } from '../../organisms/Kitchen/hooks' +import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' +import { generateNewProtocol } from '../../labware-ingred/actions' +import { DefineLiquidsModal, ProtocolMetadataNav } from '../../organisms' +import { SettingsIcon } from '../../molecules' +import { getFileMetadata } from '../../file-data/selectors' import { DeckSetupContainer } from './DeckSetup' +import { selectors } from '../../labware-ingred/selectors' +import { OffDeck } from './Offdeck' +import { LiquidsOverflowMenu } from './LiquidsOverflowMenu' + +import type { CutoutId } from '@opentrons/shared-data' +import type { DeckSlot } from '@opentrons/step-generation' + +export interface OpenSlot { + cutoutId: CutoutId + slot: DeckSlot +} export function Designer(): JSX.Element { - const { t } = useTranslation(['starting_deck_state', 'protocol_steps']) + const { t } = useTranslation([ + 'starting_deck_state', + 'protocol_steps', + 'shared', + ]) + const { bakeToast, makeSnackbar } = useKitchen() + const navigate = useNavigate() + const dispatch = useDispatch() + const fileMetadata = useSelector(getFileMetadata) + const zoomIn = useSelector(selectors.getZoomedInSlot) + const deckSetup = useSelector(getDeckSetupForActiveItem) + const isNewProtocol = useSelector(selectors.getIsNewProtocol) + const [liquidOverflowMenu, showLiquidOverflowMenu] = React.useState( + false + ) + const [showDefineLiquidModal, setDefineLiquidModal] = React.useState( + false + ) const [tab, setTab] = React.useState<'startingDeck' | 'protocolSteps'>( 'startingDeck' ) + const leftString = t('onDeck') + const rightString = t('offDeck') + + const [deckView, setDeckView] = React.useState< + typeof leftString | typeof rightString + >(leftString) + + const { modules, additionalEquipmentOnDeck } = deckSetup + + const hasTrashEntity = Object.values(additionalEquipmentOnDeck).some( + ae => ae.name === 'trashBin' || ae.name === 'wasteChute' + ) + const startingDeckTab = { text: t('protocol_starting_deck'), isActive: tab === 'startingDeck', @@ -21,19 +84,141 @@ export function Designer(): JSX.Element { text: t('protocol_steps:protocol_steps'), isActive: tab === 'protocolSteps', onClick: () => { - setTab('protocolSteps') + if (hasTrashEntity) { + setTab('protocolSteps') + } else { + makeSnackbar(t('trash_required') as string) + } }, } + const hasHardware = + (modules != null && Object.values(modules).length > 0) || + // greater than 1 to account for the default loaded trashBin + Object.values(additionalEquipmentOnDeck).length > 1 + + // only display toast if its a newly made protocol and has hardware + React.useEffect(() => { + if (hasHardware && isNewProtocol) { + bakeToast(t('add_rest') as string, INFO_TOAST, { + heading: t('we_added_hardware'), + closeButton: true, + }) + dispatch(generateNewProtocol({ isNewProtocol: false })) + } + }, []) + + React.useEffect(() => { + if (fileMetadata?.created == null) { + console.warn( + 'fileMetadata was refreshed while on the designer page, redirecting to landing page' + ) + navigate('/') + } + }, [fileMetadata]) + + const overflowWrapperRef = useOnClickOutside({ + onClickOutside: () => { + if (!showDefineLiquidModal) { + showLiquidOverflowMenu(false) + } + }, + }) + + const deckViewItems = + deckView === leftString ? : + return ( <> - {/* TODO: add these tabs to the nav bar potentially? */} - - {tab === 'startingDeck' ? ( - - ) : ( -
TODO wire this up
- )} + {showDefineLiquidModal ? ( + { + setDefineLiquidModal(false) + }} + /> + ) : null} + {liquidOverflowMenu ? ( + { + showLiquidOverflowMenu(false) + }} + showLiquidsModal={() => { + showLiquidOverflowMenu(false) + setDefineLiquidModal(true) + }} + /> + ) : null} + + + {zoomIn.slot != null ? null : ( + + )} + + + + { + showLiquidOverflowMenu(true) + }} + > + + + + {t('liquids')} + + + + { + if (hasTrashEntity) { + navigate('/overview') + } else { + makeSnackbar(t('trash_required') as string) + } + }} + > + {t('shared:done')} + + + + + {tab === 'startingDeck' ? ( + + {zoomIn.slot == null ? ( + + { + setDeckView(leftString) + }} + rightClick={() => { + setDeckView(rightString) + }} + /> + + ) : null} + {deckViewItems} + + ) : ( +
TODO wire this up
+ )} +
+
) } diff --git a/protocol-designer/src/pages/Designer/utils.ts b/protocol-designer/src/pages/Designer/utils.ts new file mode 100644 index 00000000000..c940e12c8d5 --- /dev/null +++ b/protocol-designer/src/pages/Designer/utils.ts @@ -0,0 +1,82 @@ +import { getPositionFromSlotId } from '@opentrons/shared-data' +import type { + AdditionalEquipmentName, + DeckSlot, +} from '@opentrons/step-generation' +import type { CoordinateTuple, DeckDefinition } from '@opentrons/shared-data' +import type { + AllTemporalPropertiesForTimelineFrame, + LabwareOnDeck, + ModuleOnDeck, +} from '../../step-forms' +import type { Fixture } from './DeckSetup/constants' + +interface AdditionalEquipment { + name: AdditionalEquipmentName + id: string + location?: string +} + +interface SlotInformation { + slotPosition: CoordinateTuple | null + createdModuleForSlot?: ModuleOnDeck + createdLabwareForSlot?: LabwareOnDeck + createdNestedLabwareForSlot?: LabwareOnDeck + createFixtureForSlots?: AdditionalEquipment[] + preSelectedFixture?: Fixture +} + +interface SlotInformationProps { + deckSetup: AllTemporalPropertiesForTimelineFrame + slot: DeckSlot + deckDef?: DeckDefinition +} + +const FOURTH_COLUMN_SLOTS = ['A4', 'B4', 'C4', 'D4'] +const FOURTH_COLUMN_CONVERSION = { A4: 'A3', B4: 'B3', C4: 'C3', D4: 'D3' } + +export const getSlotInformation = ( + props: SlotInformationProps +): SlotInformation => { + const { slot, deckSetup, deckDef } = props + const slotPosition = + deckDef != null ? getPositionFromSlotId(slot, deckDef) ?? null : null + const { + labware: deckSetupLabware, + modules: deckSetupModules, + additionalEquipmentOnDeck, + } = deckSetup + const createdModuleForSlot = Object.values(deckSetupModules).find( + module => module.slot === slot + ) + const createdLabwareForSlot = Object.values(deckSetupLabware).find( + lw => lw.slot === slot || lw.slot === createdModuleForSlot?.id + ) + const createdNestedLabwareForSlot = Object.values(deckSetupLabware).find( + lw => lw.slot === createdLabwareForSlot?.id + ) + const createFixtureForSlots = Object.values(additionalEquipmentOnDeck).filter( + ae => { + const slotKey = FOURTH_COLUMN_SLOTS.includes(slot) + ? FOURTH_COLUMN_CONVERSION[ + slot as keyof typeof FOURTH_COLUMN_CONVERSION + ] + : slot + return ae.location?.split('cutout')[1] === slotKey + } + ) + + const preSelectedFixture = + createFixtureForSlots != null && createFixtureForSlots.length === 2 + ? ('wasteChuteAndStagingArea' as Fixture) + : (createFixtureForSlots[0]?.name as Fixture) + + return { + createdModuleForSlot, + createdLabwareForSlot, + createdNestedLabwareForSlot, + createFixtureForSlots, + preSelectedFixture, + slotPosition: slotPosition, + } +} diff --git a/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx b/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx index 699bee78309..b340fc6d471 100644 --- a/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx +++ b/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx @@ -1,14 +1,18 @@ import * as React from 'react' -import { describe, it, vi, beforeEach } from 'vitest' +import { describe, it, vi, beforeEach, expect } from 'vitest' import { MemoryRouter } from 'react-router-dom' -import { screen } from '@testing-library/react' +import { screen, fireEvent } from '@testing-library/react' import { i18n } from '../../../assets/localization' import { renderWithProviders } from '../../../__testing-utils__' import { loadProtocolFile } from '../../../load-file/actions' +import { getFileMetadata } from '../../../file-data/selectors' +import { toggleNewProtocolModal } from '../../../navigation/actions' import { Landing } from '../index' vi.mock('../../../load-file/actions') +vi.mock('../../../file-data/selectors') +vi.mock('../../../navigation/actions') const render = () => { return renderWithProviders( @@ -23,6 +27,7 @@ const render = () => { describe('Landing', () => { beforeEach(() => { + vi.mocked(getFileMetadata).mockReturnValue({}) vi.mocked(loadProtocolFile).mockReturnValue(vi.fn()) }) it('renders the landing page image and text', () => { @@ -30,10 +35,11 @@ describe('Landing', () => { screen.getByLabelText('welcome image') screen.getByText('Welcome to Protocol Designer') screen.getByText( - 'A no-code solution to create protocols that x, y and z meaning for your lab and workflow.' + 'The easiest way to automate liquid handling on your Opentrons robot. No code required.' ) - screen.getByRole('button', { name: 'Create a protocol' }) - screen.getByText('Import existing protocol') + fireEvent.click(screen.getByRole('button', { name: 'Create a protocol' })) + expect(vi.mocked(toggleNewProtocolModal)).toHaveBeenCalled() + screen.getByText('Edit existing protocol') screen.getByRole('img', { name: 'welcome image' }) }) }) diff --git a/protocol-designer/src/pages/Landing/index.tsx b/protocol-designer/src/pages/Landing/index.tsx index 4161efa58e4..d0fe4acdbca 100644 --- a/protocol-designer/src/pages/Landing/index.tsx +++ b/protocol-designer/src/pages/Landing/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { NavLink, useNavigate } from 'react-router-dom' import styled from 'styled-components' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -13,21 +13,30 @@ import { StyledText, TYPOGRAPHY, } from '@opentrons/components' +import { BUTTON_LINK_STYLE } from '../../atoms' import { actions as loadFileActions } from '../../load-file' +import { getFileMetadata } from '../../file-data/selectors' +import { toggleNewProtocolModal } from '../../navigation/actions' import welcomeImage from '../../assets/images/welcome_page.png' import type { ThunkDispatch } from '../../types' export function Landing(): JSX.Element { const { t } = useTranslation('shared') const dispatch: ThunkDispatch = useDispatch() + const metadata = useSelector(getFileMetadata) const navigate = useNavigate() + + React.useEffect(() => { + if (metadata?.created != null) { + console.warn('protocol already exists, navigating to overview') + navigate('/overview') + } + }, [metadata, navigate]) + const loadFile = ( fileChangeEvent: React.ChangeEvent ): void => { - if (window.confirm(t('confirm_import') as string)) { - dispatch(loadFileActions.loadProtocolFile(fileChangeEvent)) - navigate('/overview') - } + dispatch(loadFileActions.loadProtocolFile(fileChangeEvent)) } return ( @@ -36,7 +45,7 @@ export function Landing(): JSX.Element { flexDirection={DIRECTION_COLUMN} alignItems={ALIGN_CENTER} paddingTop="14.875rem" - height="100vh" + height="calc(100vh - 56px)" width="100%" > - {t('no-code-solution')} + {t('no-code-required')} { + dispatch(toggleNewProtocolModal(true)) + }} marginY={SPACING.spacing32} buttonText={ @@ -68,9 +80,11 @@ export function Landing(): JSX.Element { /> - - {t('import_existing')} - + + + {t('edit_existing')} + +
diff --git a/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx b/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx index 687f5b4c417..610e1a53540 100644 --- a/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx +++ b/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx @@ -1,3 +1,70 @@ -import { it } from 'vitest' +import * as React from 'react' -it.todo('write test for Liquids') +import { describe, it, vi, beforeEach, expect } from 'vitest' +import { MemoryRouter } from 'react-router-dom' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors' +import { AssignLiquidsModal, ProtocolMetadataNav } from '../../../organisms' +import { LiquidsOverflowMenu } from '../../Designer/LiquidsOverflowMenu' +import { Liquids } from '..' +import type { NavigateFunction } from 'react-router-dom' + +const mockNavigate = vi.fn() + +vi.mock('../../Designer/LiquidsOverflowMenu') +vi.mock('../../../organisms') +vi.mock('../../../labware-ingred/selectors') +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useNavigate: () => mockNavigate, + } +}) + +const render = () => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + )[0] +} + +describe('Liquids', () => { + beforeEach(() => { + vi.mocked(labwareIngredSelectors.getSelectedLabwareId).mockReturnValue( + 'mockId' + ) + vi.mocked(AssignLiquidsModal).mockReturnValue( +
mock AssignLiquidsModal
+ ) + vi.mocked(ProtocolMetadataNav).mockReturnValue( +
mock ProtocolMetadataNav
+ ) + vi.mocked(LiquidsOverflowMenu).mockReturnValue( +
mock LiquidsOverflowMenu
+ ) + }) + it('calls navigate when there is no active labware', () => { + vi.mocked(labwareIngredSelectors.getSelectedLabwareId).mockReturnValue(null) + render() + expect(mockNavigate).toHaveBeenCalledWith('/designer') + }) + + it('renders nav and assign liquids modal', () => { + render() + screen.getByText('mock ProtocolMetadataNav') + screen.getByText('mock AssignLiquidsModal') + }) + + it('renders the liquids button overflow menu', () => { + render() + fireEvent.click(screen.getByText('Liquids')) + screen.getByText('mock LiquidsOverflowMenu') + }) +}) diff --git a/protocol-designer/src/pages/Liquids/index.tsx b/protocol-designer/src/pages/Liquids/index.tsx index dc97bb06111..e3d1f17c115 100644 --- a/protocol-designer/src/pages/Liquids/index.tsx +++ b/protocol-designer/src/pages/Liquids/index.tsx @@ -1,7 +1,95 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + PrimaryButton, + SPACING, + StyledText, + useOnClickOutside, +} from '@opentrons/components' +import { + AssignLiquidsModal, + DefineLiquidsModal, + ProtocolMetadataNav, +} from '../../organisms' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import { LiquidsOverflowMenu } from '../Designer/LiquidsOverflowMenu' export function Liquids(): JSX.Element { - const { t } = useTranslation('liquids') - return
{t('liquids')}
+ const { t } = useTranslation('starting_deck_state') + const navigate = useNavigate() + const selectedLabware = useSelector( + labwareIngredSelectors.getSelectedLabwareId + ) + const [liquidOverflowMenu, showLiquidOverflowMenu] = React.useState( + false + ) + const [showDefineLiquidModal, setDefineLiquidModal] = React.useState( + false + ) + const overflowWrapperRef = useOnClickOutside({ + onClickOutside: () => { + if (!showDefineLiquidModal) { + showLiquidOverflowMenu(false) + } + }, + }) + + React.useEffect(() => { + if (selectedLabware == null) { + console.warn('selectedLabware was lost, navigate to deisgner page') + navigate('/designer') + } + }) + + return ( + <> + {showDefineLiquidModal ? ( + { + setDefineLiquidModal(false) + }} + /> + ) : null} + {liquidOverflowMenu ? ( + { + showLiquidOverflowMenu(false) + }} + showLiquidsModal={() => { + showLiquidOverflowMenu(false) + setDefineLiquidModal(true) + }} + /> + ) : null} + + + + + + { + showLiquidOverflowMenu(true) + }} + > + + + + {t('liquids')} + + + + + + + + + ) } diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckThumbnail.tsx b/protocol-designer/src/pages/ProtocolOverview/DeckThumbnail.tsx new file mode 100644 index 00000000000..b37fa5897fe --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/DeckThumbnail.tsx @@ -0,0 +1,207 @@ +import * as React from 'react' +import { useSelector } from 'react-redux' +import { + ALIGN_CENTER, + BORDERS, + COLORS, + DeckFromLayers, + Flex, + FlexTrash, + JUSTIFY_CENTER, + RobotCoordinateSpaceWithRef, + SingleSlotFixture, + SlotLabels, + StagingAreaFixture, + WasteChuteFixture, + WasteChuteStagingAreaFixture, +} from '@opentrons/components' +import { + getCutoutIdForAddressableArea, + getDeckDefFromRobotType, + isAddressableAreaStandardSlot, + OT2_ROBOT_TYPE, + STAGING_AREA_CUTOUTS, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_CUTOUT, +} from '@opentrons/shared-data' +import { getRobotType } from '../../file-data/selectors' +import { getInitialDeckSetup } from '../../step-forms/selectors' +import { DeckThumbnailDetails } from './DeckThumbnailDetails' +import type { StagingAreaLocation, TrashCutoutId } from '@opentrons/components' +import type { CutoutId, DeckSlotId } from '@opentrons/shared-data' +import type { AdditionalEquipmentEntity } from '@opentrons/step-generation' + +const WASTE_CHUTE_SPACE = 30 +const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ + 'calibrationMarkings', + 'fixedBase', + 'doorStops', + 'metalFrame', + 'removalHandle', + 'removableDeckOutline', + 'screwHoles', + 'fixedTrash', +] + +const lightFill = COLORS.grey35 + +interface DeckThumbnailProps { + hoverSlot: DeckSlotId | null + setHoverSlot: React.Dispatch> +} +export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { + const { hoverSlot, setHoverSlot } = props + const initialDeckSetup = useSelector(getInitialDeckSetup) + const robotType = useSelector(getRobotType) + const deckDef = React.useMemo(() => getDeckDefFromRobotType(robotType), []) + const trash = Object.values(initialDeckSetup.additionalEquipmentOnDeck).find( + ae => ae.name === 'trashBin' + ) + const trashBinFixtures = [ + { + cutoutId: trash?.location as CutoutId, + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }, + ] + const wasteChuteFixtures = Object.values( + initialDeckSetup.additionalEquipmentOnDeck + ).filter( + aE => + WASTE_CHUTE_CUTOUT.includes(aE.location as CutoutId) && + aE.name === 'wasteChute' + ) + const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values( + initialDeckSetup.additionalEquipmentOnDeck + ).filter( + aE => + STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) && + aE.name === 'stagingArea' + ) + + const wasteChuteStagingAreaFixtures = Object.values( + initialDeckSetup.additionalEquipmentOnDeck + ).filter( + aE => + STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) && + aE.name === 'stagingArea' && + aE.location === WASTE_CHUTE_CUTOUT && + wasteChuteFixtures.length > 0 + ) + + const hasWasteChute = + wasteChuteFixtures.length > 0 || wasteChuteStagingAreaFixtures.length > 0 + + const filteredAddressableAreas = deckDef.locations.addressableAreas.filter( + aa => isAddressableAreaStandardSlot(aa.id, deckDef) + ) + return ( + + + {() => ( + <> + {robotType === OT2_ROBOT_TYPE ? ( + + ) : ( + <> + {filteredAddressableAreas.map(addressableArea => { + const cutoutId = getCutoutIdForAddressableArea( + addressableArea.id, + deckDef.cutoutFixtures + ) + return cutoutId != null ? ( + + ) : null + })} + {stagingAreaFixtures.map(fixture => ( + + ))} + {trash != null + ? trashBinFixtures.map(({ cutoutId }) => + cutoutId != null ? ( + + + + + ) : null + ) + : null} + {wasteChuteFixtures.map(fixture => ( + + ))} + {wasteChuteStagingAreaFixtures.map(fixture => ( + + ))} + + )} + areas.location as CutoutId + )} + {...{ + deckDef, + }} + /> + 0} + /> + + )} + + + ) +} diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckThumbnailDetails.tsx b/protocol-designer/src/pages/ProtocolOverview/DeckThumbnailDetails.tsx new file mode 100644 index 00000000000..dda66237feb --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/DeckThumbnailDetails.tsx @@ -0,0 +1,245 @@ +import * as React from 'react' +import values from 'lodash/values' + +import { Module } from '@opentrons/components' +import { + getAddressableAreaFromSlotId, + getLabwareHasQuirk, + getModuleDef2, + getPositionFromSlotId, + inferModuleOrientationFromXCoordinate, + isAddressableAreaStandardSlot, + THERMOCYCLER_MODULE_TYPE, +} from '@opentrons/shared-data' +import { LabwareOnDeck } from '../../components/DeckSetup/LabwareOnDeck' +import { getStagingAreaAddressableAreas } from '../../utils' +import { getSlotIdsBlockedBySpanningForThermocycler } from '../../step-forms' +import { SlotHover } from './SlotHover' +import type { + CutoutId, + DeckDefinition, + RobotType, +} from '@opentrons/shared-data' +import type { + InitialDeckSetup, + ModuleOnDeck, + LabwareOnDeck as LabwareOnDeckType, +} from '../../step-forms' + +interface DeckSetupDetailsProps { + initialDeckSetup: InitialDeckSetup + deckDef: DeckDefinition + stagingAreaCutoutIds: CutoutId[] + hover: string | null + setHover: React.Dispatch> + robotType: RobotType +} + +export const DeckThumbnailDetails = ( + props: DeckSetupDetailsProps +): JSX.Element => { + const { + initialDeckSetup, + deckDef, + stagingAreaCutoutIds, + robotType, + hover, + setHover, + } = props + const slotIdsBlockedBySpanning = getSlotIdsBlockedBySpanningForThermocycler( + initialDeckSetup, + robotType + ) + + const allLabware: LabwareOnDeckType[] = Object.keys( + initialDeckSetup.labware + ).reduce((acc, labwareId) => { + const labware = initialDeckSetup.labware[labwareId] + return getLabwareHasQuirk(labware.def, 'fixedTrash') + ? acc + : [...acc, labware] + }, []) + + const allModules: ModuleOnDeck[] = values(initialDeckSetup.modules) + + return ( + <> + {/* all modules */} + {allModules.map(moduleOnDeck => { + const slotId = moduleOnDeck.slot + + const slotPosition = getPositionFromSlotId(slotId, deckDef) + if (slotPosition == null) { + console.warn(`no slot ${slotId} for module ${moduleOnDeck.id}`) + return null + } + const moduleDef = getModuleDef2(moduleOnDeck.model) + const labwareLoadedOnModule = allLabware.find( + lw => lw.slot === moduleOnDeck.id + ) + return ( + + + {labwareLoadedOnModule != null ? ( + <> + + + + ) : null} + {labwareLoadedOnModule == null ? ( + + ) : null} + + + ) + })} + {/* all labware on deck NOT those in modules */} + {allLabware.map(labware => { + if ( + labware.slot === 'offDeck' || + allModules.some(m => m.id === labware.slot) || + allLabware.some(lab => lab.id === labware.slot) + ) + return null + + const slotPosition = getPositionFromSlotId(labware.slot, deckDef) + const slotBoundingBox = getAddressableAreaFromSlotId( + labware.slot, + deckDef + )?.boundingBox + if (slotPosition == null || slotBoundingBox == null) { + console.warn(`no slot ${labware.slot} for labware ${labware.id}!`) + return null + } + return ( + + + + + ) + })} + + {/* all nested labwares on deck */} + {allLabware.map(labware => { + if ( + allModules.some(m => m.id === labware.slot) || + labware.slot === 'offDeck' + ) + return null + if ( + deckDef.locations.addressableAreas.some( + addressableArea => addressableArea.id === labware.slot + ) + ) { + return null + } + const slotForOnTheDeck = allLabware.find(lab => lab.id === labware.slot) + ?.slot + const slotForOnMod = allModules.find(mod => mod.id === slotForOnTheDeck) + ?.slot + let slotPosition = null + if (slotForOnMod != null) { + slotPosition = getPositionFromSlotId(slotForOnMod, deckDef) + } else if (slotForOnTheDeck != null) { + slotPosition = getPositionFromSlotId(slotForOnTheDeck, deckDef) + } + if (slotPosition == null) { + console.warn(`no slot ${labware.slot} for labware ${labware.id}!`) + return null + } + const slotOnDeck = + slotForOnTheDeck != null + ? allModules.find(module => module.id === slotForOnTheDeck)?.slot + : null + return ( + + + + + ) + })} + + {/* SlotControls for all empty deck */} + {deckDef.locations.addressableAreas + .filter(addressableArea => { + const stagingAreaAddressableAreas = getStagingAreaAddressableAreas( + stagingAreaCutoutIds + ) + const addressableAreas = + isAddressableAreaStandardSlot(addressableArea.id, deckDef) || + stagingAreaAddressableAreas.includes(addressableArea.id) + return ( + addressableAreas && + !slotIdsBlockedBySpanning.includes(addressableArea.id) + ) + }) + .map(addressableArea => { + return ( + + + + ) + })} + + ) +} diff --git a/protocol-designer/src/pages/ProtocolOverview/OffdeckThumbnail.tsx b/protocol-designer/src/pages/ProtocolOverview/OffdeckThumbnail.tsx new file mode 100644 index 00000000000..b3d311845ce --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/OffdeckThumbnail.tsx @@ -0,0 +1,128 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { + ALIGN_CENTER, + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_CENTER, + LabwareRender, + OVERFLOW_SCROLL, + RobotWorkSpace, + SPACING, + StyledText, + WRAP, +} from '@opentrons/components' +import { selectors } from '../../labware-ingred/selectors' +import * as wellContentsSelectors from '../../top-selectors/well-contents' +import { getRobotType } from '../../file-data/selectors' +import { getInitialDeckSetup } from '../../step-forms/selectors' +import { wellFillFromWellContents } from '../../components/labware' +import { SlotHover } from './SlotHover' + +interface OffDeckThumbnailProps { + hover: string | null + setHover: React.Dispatch> +} +export function OffDeckThumbnail(props: OffDeckThumbnailProps): JSX.Element { + const { hover, setHover } = props + const { t, i18n } = useTranslation('starting_deck_state') + const robotType = useSelector(getRobotType) + const deckSetup = useSelector(getInitialDeckSetup) + const offDeckLabware = Object.values(deckSetup.labware).filter( + lw => lw.slot === 'offDeck' + ) + const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors) + const allWellContentsForActiveItem = useSelector( + wellContentsSelectors.getAllWellContentsForActiveItem + ) + + return ( + + {offDeckLabware.length === 0 ? ( + + + + {t('no_offdeck_labware')} + + + ) : ( + <> + + + {i18n.format(t('off_deck_labware'), 'upperCase')} + + + + + {offDeckLabware.map(lw => { + const wellContents = allWellContentsForActiveItem + ? allWellContentsForActiveItem[lw.id] + : null + const definition = lw.def + const { dimensions } = definition + return ( + + + {() => ( + <> + + + + )} + + + ) + })} + + + )} + + ) +} diff --git a/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx b/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx new file mode 100644 index 00000000000..0bee06aa452 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx @@ -0,0 +1,153 @@ +import * as React from 'react' +import { useSelector } from 'react-redux' +import { + ALIGN_CENTER, + BORDERS, + COLORS, + Flex, + JUSTIFY_CENTER, + RobotCoordsForeignObject, + SPACING, +} from '@opentrons/components' +import { + getCutoutIdForAddressableArea, + getDeckDefFromRobotType, + THERMOCYCLER_MODULE_TYPE, + FLEX_ROBOT_TYPE, +} from '@opentrons/shared-data' +import { getInitialDeckSetup } from '../../step-forms/selectors' +import type { + CoordinateTuple, + DeckSlotId, + AddressableAreaName, + RobotType, +} from '@opentrons/shared-data' + +interface SlotHoverProps { + hover: string | null + setHover: React.Dispatch> + slotId: DeckSlotId + slotPosition: CoordinateTuple | null + robotType: RobotType +} +const FOURTH_COLUMN_SLOTS = ['A4', 'B4', 'C4', 'D4'] + +export function SlotHover(props: SlotHoverProps): JSX.Element | null { + const { hover, setHover, slotId, slotPosition, robotType } = props + const deckSetup = useSelector(getInitialDeckSetup) + const { additionalEquipmentOnDeck, modules } = deckSetup + const deckDef = React.useMemo(() => getDeckDefFromRobotType(robotType), []) + const hasTCOnSlot = Object.values(modules).find( + module => module.slot === slotId && module.type === THERMOCYCLER_MODULE_TYPE + ) + const tcSlots = robotType === FLEX_ROBOT_TYPE ? ['A1'] : ['8', '10', '11'] + const stagingAreaLocations = Object.values(additionalEquipmentOnDeck) + .filter(ae => ae.name === 'stagingArea') + ?.map(ae => ae.location as string) + + const cutoutId = + getCutoutIdForAddressableArea( + slotId as AddressableAreaName, + deckDef.cutoutFixtures + ) ?? 'cutoutD1' + + // return null for TC slots + if (slotPosition === null || (hasTCOnSlot && tcSlots.includes(slotId))) + return null + + const hoverOpacity = hover != null && hover === slotId ? '1' : '0' + const slotFill = ( + + ) + + if (robotType === FLEX_ROBOT_TYPE) { + const hasStagingArea = stagingAreaLocations.includes(cutoutId) + + const X_ADJUSTMENT_LEFT_SIDE = -101.5 + const X_ADJUSTMENT = -17 + const X_DIMENSION_MIDDLE_SLOTS = 160.3 + const X_DIMENSION_OUTER_SLOTS = hasStagingArea ? 160.0 : 246.5 + const X_DIMENSION_4TH_COLUMN_SLOTS = 175.0 + const Y_DIMENSION = hasTCOnSlot ? 294.0 : 106.0 + + const slotFromCutout = slotId + const isLeftSideofDeck = + slotFromCutout === 'A1' || + slotFromCutout === 'B1' || + slotFromCutout === 'C1' || + slotFromCutout === 'D1' + const xAdjustment = isLeftSideofDeck ? X_ADJUSTMENT_LEFT_SIDE : X_ADJUSTMENT + const x = slotPosition[0] + xAdjustment + + const yAdjustment = -10 + const y = slotPosition[1] + yAdjustment + + const isMiddleOfDeck = + slotId === 'A2' || slotId === 'B2' || slotId === 'C2' || slotId === 'D2' + + let xDimension = X_DIMENSION_OUTER_SLOTS + if (isMiddleOfDeck) { + xDimension = X_DIMENSION_MIDDLE_SLOTS + } else if (FOURTH_COLUMN_SLOTS.includes(slotId)) { + xDimension = X_DIMENSION_4TH_COLUMN_SLOTS + } + const yDimension = Y_DIMENSION + + return ( + { + setHover(slotId) + }, + onMouseLeave: () => { + setHover(null) + }, + }} + > + {slotFill} + + ) + } else { + const y = slotPosition[1] + const x = slotPosition[0] + + return ( + { + setHover(slotId) + }, + onMouseLeave: () => { + setHover(null) + }, + }} + > + {slotFill} + + ) + } +} diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/DeckThumbnail.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/DeckThumbnail.test.tsx new file mode 100644 index 00000000000..8fdd677560a --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/DeckThumbnail.test.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach, expect } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { screen } from '@testing-library/react' +import { FLEX_ROBOT_TYPE, fixture12Trough } from '@opentrons/shared-data' +import { renderWithProviders } from '../../../__testing-utils__' +import { getInitialDeckSetup } from '../../../step-forms/selectors' +import { LabwareOnDeck } from '../../../components/DeckSetup/LabwareOnDeck' +import { getRobotType } from '../../../file-data/selectors' +import { DeckThumbnail } from '../DeckThumbnail' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type * as Components from '@opentrons/components' + +vi.mock('../../../components/DeckSetup/LabwareOnDeck') +vi.mock('../../../file-data/selectors') +vi.mock('../../../step-forms/selectors') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + SingleSlotFixture: () =>
mock single slot fixture
, + Module: () =>
mock module
, + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('DeckThumbnail', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + hoverSlot: null, + setHoverSlot: vi.fn(), + } + vi.mocked(LabwareOnDeck).mockReturnValue(
mock LabwareOnDeck
) + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + labware: { + labware: { + id: 'mockId', + def: fixture12Trough as LabwareDefinition2, + labwareDefURI: 'mockDefUri', + slot: 'A1', + }, + }, + }) + }) + + it('renders a flex deck with a labware and all single slot fixutres', () => { + render(props) + screen.getByText('mock LabwareOnDeck') + expect(screen.getAllByText('mock single slot fixture')).toHaveLength(12) + }) +}) diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/OffdeckThumbnail.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/OffdeckThumbnail.test.tsx new file mode 100644 index 00000000000..1a262365f4e --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/OffdeckThumbnail.test.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { FLEX_ROBOT_TYPE, fixture12Trough } from '@opentrons/shared-data' +import { screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { getRobotType } from '../../../file-data/selectors' +import { selectors } from '../../../labware-ingred/selectors' +import { getInitialDeckSetup } from '../../../step-forms/selectors' +import { getAllWellContentsForActiveItem } from '../../../top-selectors/well-contents' +import { OffDeckThumbnail } from '../OffdeckThumbnail' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type * as Components from '@opentrons/components' + +vi.mock('../../../top-selectors/well-contents') +vi.mock('../../../labware-ingred/selectors') +vi.mock('../../../step-forms/selectors') +vi.mock('../../../file-data/selectors') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + LabwareRender: () =>
mock LabwareRender
, + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('OffDeckThumbnail', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + hover: null, + setHover: vi.fn(), + } + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + labware: { + labware: { + id: 'mockId', + def: fixture12Trough as LabwareDefinition2, + labwareDefURI: 'mockDefUri', + slot: 'offDeck', + }, + }, + }) + vi.mocked(selectors.getLiquidDisplayColors).mockReturnValue([]) + vi.mocked(getAllWellContentsForActiveItem).mockReturnValue({}) + }) + + it('renders off-deck overview with 1 labware', () => { + render(props) + screen.getByText('OFF-DECK LABWARE') + screen.getByText('mock LabwareRender') + }) + it('renders the empty state with no off-deck labware', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + labware: {}, + }) + render(props) + screen.getByText('No off-deck labware added') + }) +}) diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx index 867fce01306..81840da58f0 100644 --- a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx @@ -2,17 +2,34 @@ import * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + +import { EditProtocolMetadataModal } from '../../../organisms' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../assets/localization' import { getFileMetadata, getRobotType } from '../../../file-data/selectors' -import { getInitialDeckSetup } from '../../../step-forms/selectors' +import { + getAdditionalEquipmentEntities, + getInitialDeckSetup, + getSavedStepForms, +} from '../../../step-forms/selectors' +import { useBlockingHint } from '../../../components/Hints/useBlockingHint' +import { MaterialsListModal } from '../../../organisms/MaterialsListModal' +import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors' import { ProtocolOverview } from '../index' +import { DeckThumbnail } from '../DeckThumbnail' +import { OffDeckThumbnail } from '../OffdeckThumbnail' import type { NavigateFunction } from 'react-router-dom' +vi.mock('../OffdeckThumbnail') +vi.mock('../DeckThumbnail') vi.mock('../../../step-forms/selectors') vi.mock('../../../file-data/selectors') - +vi.mock('../../../components/Hints/useBlockingHint') +vi.mock('../../../organisms/MaterialsListModal') +vi.mock('../../../labware-ingred/selectors') +vi.mock('../../../organisms') +vi.mock('../../../labware-ingred/selectors') const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { @@ -31,6 +48,13 @@ const render = () => { describe('ProtocolOverview', () => { beforeEach(() => { + vi.mocked(getAdditionalEquipmentEntities).mockReturnValue({}) + vi.mocked(getSavedStepForms).mockReturnValue({ + __INITIAL_DECK_SETUP_STEP__: {} as any, + }) + vi.mocked(labwareIngredSelectors.allIngredientGroupFields).mockReturnValue( + {} + ) vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) vi.mocked(getInitialDeckSetup).mockReturnValue({ pipettes: {}, @@ -42,13 +66,28 @@ describe('ProtocolOverview', () => { protocolName: 'mockName', author: 'mockAuthor', description: 'mockDescription', + created: 123, }) + vi.mocked(useBlockingHint).mockReturnValue(null) + vi.mocked(MaterialsListModal).mockReturnValue( +
mock MaterialsListModal
+ ) + vi.mocked(DeckThumbnail).mockReturnValue(
mock DeckThumbnail
) + vi.mocked(OffDeckThumbnail).mockReturnValue( +
mock OffdeckThumbnail
+ ) }) + it('renders each section with text', () => { render() + // buttons + screen.getByRole('button', { name: 'Edit protocol' }) + screen.getByRole('button', { name: 'Export protocol' }) + screen.getByText('Materials list') + // metadata screen.getByText('mockName') - screen.getByText('Protocol metadata') + screen.getByText('Protocol Metadata') screen.getAllByText('Edit') screen.getByText('Description') screen.getByText('mockDescription') @@ -56,6 +95,8 @@ describe('ProtocolOverview', () => { screen.getByText('mockAuthor') screen.getByText('Date created') screen.getByText('Last exported') + screen.getByText('Required app version') + screen.getByText('8.0.0 or higher') // instruments screen.getByText('Instruments') screen.getByText('Robot type') @@ -64,14 +105,55 @@ describe('ProtocolOverview', () => { screen.getByText('Right pipette') screen.getByText('Extension mount') // liquids - screen.getByText('Liquids') + screen.getByText('Liquid Definitions') // steps screen.getByText('Protocol steps') }) - it('navigates to deck setup deck setup', () => { + it('should render the deck thumbnail and offdeck thumbnail', () => { + render() + screen.getByText('mock DeckThumbnail') + fireEvent.click(screen.getByText('Off-deck')) + screen.getByText('mock OffdeckThumbnail') + }) + + it('should render text N/A if there is no data', () => { + vi.mocked(getFileMetadata).mockReturnValue({ + protocolName: undefined, + author: undefined, + description: undefined, + }) + render() + expect(screen.getAllByText('N/A').length).toBe(7) + }) + + it('navigates to starting deck state', () => { + render() + const button = screen.getByRole('button', { name: 'Edit protocol' }) + fireEvent.click(button) + expect(mockNavigate).toHaveBeenCalledWith('/designer') + }) + + it('renders the file sidebar and exports with blocking hint for exporting', () => { + vi.mocked(useBlockingHint).mockReturnValue(
mock blocking hint
) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export protocol' })) + expect(vi.mocked(useBlockingHint)).toHaveBeenCalled() + screen.getByText('mock blocking hint') + }) + + it('render mock materials list modal when clicking materials list', () => { + render() + fireEvent.click(screen.getByText('Materials list')) + screen.getByText('mock MaterialsListModal') + }) + + it('renders the edit protocol metadata modal', () => { + vi.mocked(EditProtocolMetadataModal).mockReturnValue( +
mock EditProtocolMetadataModal
+ ) render() - fireEvent.click(screen.getByTestId('toDeckSetup')) - expect(mockNavigate).toHaveBeenCalled() + fireEvent.click(screen.getByTestId('ProtocolOverview_MetadataEditButton')) + screen.getByText('mock EditProtocolMetadataModal') }) }) diff --git a/protocol-designer/src/pages/ProtocolOverview/index.tsx b/protocol-designer/src/pages/ProtocolOverview/index.tsx index 6f38df76d63..3512db95f3a 100644 --- a/protocol-designer/src/pages/ProtocolOverview/index.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/index.tsx @@ -1,41 +1,183 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { format } from 'date-fns' +import { css } from 'styled-components' + import { + ALIGN_CENTER, Btn, DIRECTION_COLUMN, Flex, + InfoScreen, + JUSTIFY_FLEX_END, JUSTIFY_SPACE_BETWEEN, + LargeButton, + LiquidIcon, ListItem, ListItemDescriptor, SPACING, StyledText, TYPOGRAPHY, + ToggleGroup, } from '@opentrons/components' -import { FLEX_ROBOT_TYPE, getPipetteSpecsV2 } from '@opentrons/shared-data' -import { getInitialDeckSetup } from '../../step-forms/selectors' +import { + getPipetteSpecsV2, + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, +} from '@opentrons/shared-data' +import { + getAdditionalEquipmentEntities, + getInitialDeckSetup, +} from '../../step-forms/selectors' import { selectors as fileSelectors } from '../../file-data' -import { getRobotType } from '../../file-data/selectors' -import type { PipetteName } from '@opentrons/shared-data' +import { selectors as stepFormSelectors } from '../../step-forms' +import { actions as loadFileActions } from '../../load-file' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import { + getUnusedEntities, + getUnusedStagingAreas, + getUnusedTrash, +} from '../../components/FileSidebar/utils' +import { resetScrollElements } from '../../ui/steps/utils' +import { useBlockingHint } from '../../components/Hints/useBlockingHint' +import { v8WarningContent } from '../../components/FileSidebar/FileSidebar' +import { MaterialsListModal } from '../../organisms/MaterialsListModal' +import { BUTTON_LINK_STYLE } from '../../atoms' +import { + EditProtocolMetadataModal, + EditInstrumentsModal, + SlotDetailsContainer, +} from '../../organisms' +import { DeckThumbnail } from './DeckThumbnail' +import { OffDeckThumbnail } from './OffdeckThumbnail' + +import type { CreateCommand, PipetteName } from '@opentrons/shared-data' +import type { DeckSlot } from '@opentrons/step-generation' +import type { ThunkDispatch } from '../../types' +import type { HintKey } from '../../tutorial' -const DATE_ONLY_FORMAT = 'MMM dd, yyyy' -const DATETIME_FORMAT = 'MMM dd, yyyy | h:mm a' +const REQUIRED_APP_VERSION = '8.0.0' +const DATE_ONLY_FORMAT = 'MMMM dd, yyyy' +const DATETIME_FORMAT = 'MMMM dd, yyyy | h:mm a' + +const LOAD_COMMANDS: Array = [ + 'loadLabware', + 'loadModule', + 'loadPipette', + 'loadLiquid', +] + +interface Fixture { + trashBin: boolean + wasteChute: boolean + stagingAreaSlots: string[] +} export function ProtocolOverview(): JSX.Element { - const { t } = useTranslation(['protocol_overview', 'shared']) + const { t } = useTranslation([ + 'protocol_overview', + 'alert', + 'shared', + 'starting_deck_State', + ]) const navigate = useNavigate() + const [ + showEditInstrumentsModal, + setShowEditInstrumentsModal, + ] = React.useState(false) + const [ + showEditMetadataModal, + setShowEditMetadataModal, + ] = React.useState(false) const formValues = useSelector(fileSelectors.getFileMetadata) - const robotType = useSelector(getRobotType) - const deckSetup = useSelector(getInitialDeckSetup) - const additionalEquipmentOnDeck = Object.values( - deckSetup.additionalEquipmentOnDeck + const robotType = useSelector(fileSelectors.getRobotType) + const initialDeckSetup = useSelector(getInitialDeckSetup) + const allIngredientGroupFields = useSelector( + labwareIngredSelectors.allIngredientGroupFields + ) + const dispatch: ThunkDispatch = useDispatch() + const [hover, setHover] = React.useState(null) + const [showBlockingHint, setShowBlockingHint] = React.useState(false) + const [ + showMaterialsListModal, + setShowMaterialsListModal, + ] = React.useState(false) + const fileData = useSelector(fileSelectors.createFile) + const savedStepForms = useSelector(stepFormSelectors.getSavedStepForms) + const additionalEquipment = useSelector(getAdditionalEquipmentEntities) + const liquidsOnDeck = useSelector( + labwareIngredSelectors.allIngredientNamesIds + ) + const leftString = t('starting_deck_state:onDeck') + const rightString = t('starting_deck_state:offDeck') + + const [deckView, setDeckView] = React.useState< + typeof leftString | typeof rightString + >(leftString) + + React.useEffect(() => { + if (formValues?.created == null) { + console.warn( + 'formValues was refreshed while on the overview page, redirecting to landing page' + ) + navigate('/') + } + }, [formValues]) + + const { + modules: modulesOnDeck, + labware: labwaresOnDeck, + additionalEquipmentOnDeck, + pipettes, + } = initialDeckSetup + const isOffDeckHover = hover != null && labwaresOnDeck[hover] != null + + const nonLoadCommands = + fileData?.commands.filter( + command => !LOAD_COMMANDS.includes(command.commandType) + ) ?? [] + const gripperInUse = + fileData?.commands.find( + command => + command.commandType === 'moveLabware' && + command.params.strategy === 'usingGripper' + ) != null + const noCommands = fileData != null ? nonLoadCommands.length === 0 : true + const modulesWithoutStep = getUnusedEntities( + modulesOnDeck, + savedStepForms, + 'moduleId', + robotType + ) + const pipettesWithoutStep = getUnusedEntities( + initialDeckSetup.pipettes, + savedStepForms, + 'pipette', + robotType + ) + const isGripperAttached = Object.values(additionalEquipment).some( + equipment => equipment?.name === 'gripper' + ) + const gripperWithoutStep = isGripperAttached && !gripperInUse + + const { trashBinUnused, wasteChuteUnused } = getUnusedTrash( + additionalEquipmentOnDeck, + fileData?.commands ) - const pipettesOnDeck = Object.values(deckSetup.pipettes) + const fixtureWithoutStep: Fixture = { + trashBin: trashBinUnused, + wasteChute: wasteChuteUnused, + stagingAreaSlots: getUnusedStagingAreas( + additionalEquipmentOnDeck, + fileData?.commands + ), + } + + const pipettesOnDeck = Object.values(pipettes) const leftPip = pipettesOnDeck.find(pip => pip.mount === 'left') const rightPip = pipettesOnDeck.find(pip => pip.mount === 'right') - const gripper = additionalEquipmentOnDeck.find(ae => ae.name === 'gripper') const { protocolName, description, @@ -44,185 +186,379 @@ export function ProtocolOverview(): JSX.Element { author, } = formValues const metaDataInfo = [ - { description: description }, - { author: author }, - { created: created != null ? format(created, DATE_ONLY_FORMAT) : 'N/A' }, + { description }, + { author }, + { created: created != null ? format(created, DATE_ONLY_FORMAT) : t('na') }, { modified: - lastModified != null ? format(lastModified, DATETIME_FORMAT) : 'N/A', + lastModified != null ? format(lastModified, DATETIME_FORMAT) : t('na'), }, ] + const hasWarning = + noCommands || + modulesWithoutStep.length > 0 || + pipettesWithoutStep.length > 0 || + gripperWithoutStep || + fixtureWithoutStep.trashBin || + fixtureWithoutStep.wasteChute || + fixtureWithoutStep.stagingAreaSlots.length > 0 + + const getExportHintContent = (): { + hintKey: HintKey + content: React.ReactNode + } => { + return { + hintKey: t('alert:export_v8_1_protocol_7_3'), + content: v8WarningContent(t), + } + } + + const { hintKey, content } = getExportHintContent() + + const blockingExportHint = useBlockingHint({ + enabled: showBlockingHint, + hintKey, + content, + handleCancel: () => { + setShowBlockingHint(false) + }, + handleContinue: () => { + setShowBlockingHint(false) + dispatch(loadFileActions.saveProtocolFile()) + }, + }) + return ( - - - - {protocolName ?? t('untitled_protocol')} - - - - - - + {showEditMetadataModal ? ( + { + setShowEditMetadataModal(false) + }} + /> + ) : null} + {showEditInstrumentsModal ? ( + { + setShowEditInstrumentsModal(false) + }} + /> + ) : null} + {blockingExportHint} + {showMaterialsListModal ? ( + + ) : null} + + + + - - {t('protocol_metadata')} - - { - console.log('wire this up') - }} - > - - {t('edit')} - - - - - {metaDataInfo.map(info => { - const [title, value] = Object.entries(info)[0] - return ( - - - - ) - })} - + {protocolName != null && protocolName !== '' + ? protocolName + : t('untitled_protocol')} + - - - - {t('instruments')} - - { - console.log('wire this up') - }} + + + { + navigate('/designer') + }} + whiteSpace="nowrap" + height="3.5rem" + /> + { + // ToDo (kk:08/26/2024) should use hasWarning later + if (!hasWarning) { + resetScrollElements() + // ToDo (kk:08/26/2024) create warning modal + } else { + resetScrollElements() + setShowBlockingHint(true) + } + }} + iconName="arrow-right" + whiteSpace="nowrap" + /> + + + + + + - - {t('edit')} + + {t('protocol_metadata')} - - - - - - - - - - + { + setShowEditMetadataModal(true) + }} + css={BUTTON_LINK_STYLE} + data-testid="ProtocolOverview_MetadataEditButton" + > + + {t('edit')} + + + + + {metaDataInfo.map(info => { + const [title, value] = Object.entries(info)[0] + + return ( + + + + ) + })} + + - {robotType === FLEX_ROBOT_TYPE ? ( - + + + + + {t('instruments')} + + { + setShowEditInstrumentsModal(true) + }} + css={BUTTON_LINK_STYLE} + > + + {t('edit')} + + + + + + + + + + + - ) : null} + {robotType === FLEX_ROBOT_TYPE ? ( + + + + ) : null} + - - - - - {t('liquids')} - + + + + {t('liquid_defs')} + + + + {Object.keys(allIngredientGroupFields).length > 0 ? ( + Object.values(allIngredientGroupFields).map( + (liquid, index) => ( + + + + + {liquid.name} + + + } + content={liquid.description ?? t('na')} + /> + + ) + ) + ) : ( + + )} + - - - - + + + + {t('step')} + + + + {Object.keys(savedStepForms).length <= 1 ? ( + + ) : ( + + + + )} + - - - - {t('step')} - - - - - - + + + + + {t('starting_deck')} + + { + setShowMaterialsListModal(true) + }} + css={BUTTON_LINK_STYLE} + > + + {t('materials_list')} + + + + { + setDeckView(leftString) + }} + rightClick={() => { + setDeckView(rightString) + }} + /> - - - - - - {t('starting_deck')} - - { - navigate('/designer') - }} + - - {t('edit')} - - - - - TODO: wire this up + {deckView === leftString ? ( + + ) : ( + + )} + + - + ) } + +const PROTOCOL_NAME_TEXT_STYLE = css` + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + -webkit-line-clamp: 3; +` diff --git a/protocol-designer/src/pages/Settings/__tests__/Settings.test.tsx b/protocol-designer/src/pages/Settings/__tests__/Settings.test.tsx new file mode 100644 index 00000000000..787cb3c6e4a --- /dev/null +++ b/protocol-designer/src/pages/Settings/__tests__/Settings.test.tsx @@ -0,0 +1,82 @@ +import * as React from 'react' + +import { describe, it, vi, beforeEach, expect } from 'vitest' +import { MemoryRouter } from 'react-router-dom' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { getHasOptedIn } from '../../../analytics/selectors' +import { getFeatureFlagData } from '../../../feature-flags/selectors' +import { getCanClearHintDismissals } from '../../../tutorial/selectors' +import { clearAllHintDismissals } from '../../../tutorial/actions' +import { optIn } from '../../../analytics/actions' +import { setFeatureFlags } from '../../../feature-flags/actions' +import { Settings } from '..' + +vi.mock('../../../feature-flags/actions') +vi.mock('../../../analytics/actions') +vi.mock('../../../tutorial/actions') +vi.mock('../../../tutorial/selectors') +vi.mock('../../../feature-flags/selectors') +vi.mock('../../../analytics/selectors') +const render = () => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + )[0] +} + +describe('Settings', () => { + beforeEach(() => { + vi.mocked(getHasOptedIn).mockReturnValue(false) + vi.mocked(getFeatureFlagData).mockReturnValue({}) + vi.mocked(getCanClearHintDismissals).mockReturnValue(true) + }) + it('renders the settings page without the dev ffs visible', () => { + render() + screen.getByText('Settings') + screen.getByText('App settings') + screen.getByText('Protocol designer version') + screen.getByText('fake_PD_version') + screen.getByText('User settings') + screen.getByText('Hints') + screen.getByText('Reset all hints and tips notifications') + screen.getByText('Reset hints') + screen.getByText('Privacy') + screen.getByText('Share sessions with Opentrons') + screen.getByText( + 'We’re working to improve Protocol Designer. Part of the process involves watching real user sessions to understand which parts of the interface are working and which could use improvement. We never share sessions outside of Opentrons.' + ) + }) + it('renders the hints button and calls to dismiss them when text is pressed', () => { + render() + fireEvent.click(screen.getByText('Reset hints')) + expect(vi.mocked(clearAllHintDismissals)).toHaveBeenCalled() + }) + it('renders the analytics toggle and calls the action when pressed', () => { + render() + fireEvent.click(screen.getByTestId('analyticsToggle')) + expect(vi.mocked(optIn)).toHaveBeenCalled() + }) + it('renders the dev ffs section when prerelease mode is turned on', () => { + vi.mocked(getFeatureFlagData).mockReturnValue({ + PRERELEASE_MODE: true, + OT_PD_DISABLE_MODULE_RESTRICTIONS: true, + }) + + render() + screen.getByText('Developer feature flags') + screen.getByText('Use prerelease mode') + screen.getByText('Show in-progress features for testing & internal use') + screen.getByText('Disable module placement restrictions') + screen.getByText( + 'Turn off all restrictions on module placement and related pipette crash guidance.' + ) + fireEvent.click(screen.getByTestId('btn_PRERELEASE_MODE')) + expect(vi.mocked(setFeatureFlags)).toHaveBeenCalled() + }) +}) diff --git a/protocol-designer/src/pages/Settings/index.tsx b/protocol-designer/src/pages/Settings/index.tsx new file mode 100644 index 00000000000..2baa3167ba9 --- /dev/null +++ b/protocol-designer/src/pages/Settings/index.tsx @@ -0,0 +1,264 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { css } from 'styled-components' +import { + ALIGN_CENTER, + BORDERS, + Btn, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { + actions as analyticsActions, + selectors as analyticsSelectors, +} from '../../analytics' +import { + actions as tutorialActions, + selectors as tutorialSelectors, +} from '../../tutorial' +import { actions as featureFlagActions } from '../../feature-flags' +import { getFeatureFlagData } from '../../feature-flags/selectors' +import type { FlagTypes } from '../../feature-flags' + +export function Settings(): JSX.Element { + const dispatch = useDispatch() + const { t } = useTranslation(['feature_flags', 'shared']) + const hasOptedIn = useSelector(analyticsSelectors.getHasOptedIn) + const flags = useSelector(getFeatureFlagData) + const canClearHintDismissals = useSelector( + tutorialSelectors.getCanClearHintDismissals + ) + const _toggleOptedIn = hasOptedIn + ? analyticsActions.optOut + : analyticsActions.optIn + + const prereleaseModeEnabled = flags.PRERELEASE_MODE === true + + const allFlags = Object.keys(flags) as FlagTypes[] + + const getDescription = (flag: FlagTypes): string => { + return flag === 'OT_PD_DISABLE_MODULE_RESTRICTIONS' + ? t(`feature_flags:${flag}.description_1`) + : t(`feature_flags:${flag}.description`) + } + + const setFeatureFlags = ( + flags: Partial> + ): void => { + dispatch(featureFlagActions.setFeatureFlags(flags)) + } + + const toFlagRow = (flagName: FlagTypes): JSX.Element => { + const iconName = Boolean(flags[flagName]) + ? 'ot-toggle-input-on' + : 'ot-toggle-input-off' + + return ( + + + + {t(`feature_flags:${flagName}.title`)} + + + {getDescription(flagName)} + + + { + setFeatureFlags({ + [flagName as string]: !flags[flagName], + }) + }} + > + + + + ) + } + + const prereleaseFlagRows = allFlags.map(toFlagRow) + + return ( + + + + {t('shared:settings')} + + + + {t('shared:app_settings')} + + + + {t('shared:pd_version')} + + + {process.env.OT_PD_VERSION} + + + + + + {t('shared:user_settings')} + + + + + {t('shared:hints')} + + + + {t('shared:reset_hints_and_tips')} + + + + dispatch(tutorialActions.clearAllHintDismissals())} + > + + {canClearHintDismissals + ? t('shared:reset_hints') + : t('shared:no_hints_to_restore')} + + + + + + + {t('shared:privacy')} + + + + + {t('shared:shared_sessions')} + + + + {t('shared:we_are_improving')} + + + + dispatch(_toggleOptedIn())} + > + + + + + {prereleaseModeEnabled ? ( + + + {t('shared:developer_ff')} + + + {prereleaseFlagRows} + + + ) : null} + + + ) +} + +const TOGGLE_DISABLED_STYLES = css` + color: ${COLORS.grey50}; + + &:hover { + color: ${COLORS.grey55}; + } + + &:focus-visible { + box-shadow: 0 0 0 3px ${COLORS.yellow50}; + } + + &:disabled { + color: ${COLORS.grey30}; + } +` + +const TOGGLE_ENABLED_STYLES = css` + color: ${COLORS.blue50}; + + &:hover { + color: ${COLORS.blue55}; + } + + &:focus-visible { + box-shadow: 0 0 0 3px ${COLORS.yellow50}; + } + + &:disabled { + color: ${COLORS.grey30}; + } +` diff --git a/protocol-designer/src/step-forms/utils/index.ts b/protocol-designer/src/step-forms/utils/index.ts index 81ef3688a7b..aa880038bbe 100644 --- a/protocol-designer/src/step-forms/utils/index.ts +++ b/protocol-designer/src/step-forms/utils/index.ts @@ -9,6 +9,7 @@ import { THERMOCYCLER_MODULE_V2, WASTE_CHUTE_CUTOUT, FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import { SPAN7_8_10_11_SLOT, TC_SPAN_SLOTS } from '../../constants' import { hydrateField } from '../../steplist/fieldLevel' @@ -22,6 +23,7 @@ import type { LoadModuleCreateCommand, ModuleType, MoveLabwareCreateCommand, + RobotType, } from '@opentrons/shared-data' import type { NormalizedPipette, @@ -154,6 +156,7 @@ export function denormalizePipetteEntities( {} ) } +/** deprecated */ export const getSlotIdsBlockedBySpanning = ( initialDeckSetup: InitialDeckSetup ): DeckSlot[] => { @@ -168,6 +171,22 @@ export const getSlotIdsBlockedBySpanning = ( return [] } +export const getSlotIdsBlockedBySpanningForThermocycler = ( + initialDeckSetup: InitialDeckSetup, + robotType: RobotType +): DeckSlot[] => { + const loadedThermocycler = values(initialDeckSetup.modules).find( + ({ type }: ModuleOnDeck) => type === THERMOCYCLER_MODULE_TYPE + ) + if (loadedThermocycler != null && robotType === FLEX_ROBOT_TYPE) { + return ['A1', 'B1'] + } else if (loadedThermocycler != null && robotType === OT2_ROBOT_TYPE) { + return ['7', '8', '10', '11'] + } + + return [] +} + // TODO(jr, 3/13/24): refactor this util it is messy and confusing export const getSlotIsEmpty = ( initialDeckSetup: InitialDeckSetup, diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index 30b62883ec9..52fd34ec9ed 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -67,7 +67,9 @@ export const COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE: Record< 'armadillo_96_wellplate_200ul_pcr_full_skirt', 'biorad_96_wellplate_200ul_pcr', ], - [ABSORBANCE_READER_TYPE]: [], + [ABSORBANCE_READER_TYPE]: [ + 'opentrons_flex_lid_absorbance_plate_reader_module', + ], } export const getLabwareIsCompatible = ( def: LabwareDefinition2, diff --git a/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts b/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts index ea05e64306a..be8faca4dc6 100644 --- a/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts +++ b/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts @@ -50,16 +50,21 @@ export function useCreateMaintenanceCommandMutation(): UseCreateMaintenanceComma createMaintenanceCommand(host as HostConfig, maintenanceRunId, command, { waitUntilComplete, timeout, - }).then(response => { - queryClient - .invalidateQueries([host, 'maintenance_runs']) - .catch((e: Error) => { - console.error( - `error invalidating maintenance runs query: ${e.message}` - ) - }) - return response.data }) + .then(response => { + queryClient + .invalidateQueries([host, 'maintenance_runs']) + .catch((e: Error) => { + console.error( + `error invalidating maintenance runs query: ${e.message}` + ) + }) + return response.data + }) + .catch((e: any) => { + queryClient.invalidateQueries([host, 'robot/control/estopStatus']) + throw e + }) ) return { diff --git a/react-api-client/src/maintenance_runs/useCreateMaintenanceRunMutation.ts b/react-api-client/src/maintenance_runs/useCreateMaintenanceRunMutation.ts index 298c0576587..5bdbf076f37 100644 --- a/react-api-client/src/maintenance_runs/useCreateMaintenanceRunMutation.ts +++ b/react-api-client/src/maintenance_runs/useCreateMaintenanceRunMutation.ts @@ -1,5 +1,5 @@ import { createMaintenanceRun } from '@opentrons/api-client' -import { useMutation } from 'react-query' +import { useMutation, useQueryClient } from 'react-query' import { useHost } from '../api' import type { AxiosError } from 'axios' import type { @@ -40,6 +40,7 @@ export function useCreateMaintenanceRunMutation( const contextHost = useHost() const host = hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost + const queryClient = useQueryClient() const mutation = useMutation< MaintenanceRun, AxiosError, @@ -50,6 +51,7 @@ export function useCreateMaintenanceRunMutation( createMaintenanceRun(host as HostConfig, createMaintenanceRunData) .then(response => response.data) .catch(e => { + queryClient.invalidateQueries([host, 'robot/control/estopStatus']) throw e }), options diff --git a/react-api-client/src/robot/useAcknowledgeEstopDisengageMutation.ts b/react-api-client/src/robot/useAcknowledgeEstopDisengageMutation.ts index 90585699e80..b8790fd18ae 100644 --- a/react-api-client/src/robot/useAcknowledgeEstopDisengageMutation.ts +++ b/react-api-client/src/robot/useAcknowledgeEstopDisengageMutation.ts @@ -1,7 +1,7 @@ -import { useMutation } from 'react-query' +import { useMutation, useQueryClient } from 'react-query' import { acknowledgeEstopDisengage } from '@opentrons/api-client' import { useHost } from '../api' -import type { AxiosError } from 'axios' +import type { AxiosError, AxiosResponse } from 'axios' import type { UseMutationResult, UseMutateFunction, @@ -29,15 +29,23 @@ export function useAcknowledgeEstopDisengageMutation( const contextHost = useHost() const host = hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost - + const queryClient = useQueryClient() const mutation = useMutation( [host, 'robot/control/acknowledgeEstopDisengage'], - () => - acknowledgeEstopDisengage(host as HostConfig) - .then(response => response.data) - .catch(e => { + () => { + return acknowledgeEstopDisengage(host as HostConfig) + .then((response: AxiosResponse) => { + queryClient.setQueryData( + [host, 'robot/control/estopStatus'], + response.data + ) + return response.data + }) + .catch((e: any) => { + queryClient.invalidateQueries([host, 'robot/control/estopStatus']) throw e - }), + }) + }, options ) diff --git a/react-api-client/src/runs/useAllCommandsAsPreSerializedList.ts b/react-api-client/src/runs/useAllCommandsAsPreSerializedList.ts index 4d0a34295d0..4f7e2fdc0e0 100644 --- a/react-api-client/src/runs/useAllCommandsAsPreSerializedList.ts +++ b/react-api-client/src/runs/useAllCommandsAsPreSerializedList.ts @@ -7,21 +7,21 @@ import { useHost } from '../api' import type { UseQueryOptions, UseQueryResult } from 'react-query' import type { - GetCommandsParams, + GetRunCommandsParams, HostConfig, CommandsData, RunCommandSummary, } from '@opentrons/api-client' const DEFAULT_PAGE_LENGTH = 30 -export const DEFAULT_PARAMS: GetCommandsParams = { +export const DEFAULT_PARAMS: GetRunCommandsParams = { cursor: null, pageLength: DEFAULT_PAGE_LENGTH, } export function useAllCommandsAsPreSerializedList( runId: string | null, - params?: GetCommandsParams | null, + params?: GetRunCommandsParams | null, options: UseQueryOptions = {} ): UseQueryResult { const host = useHost() @@ -32,6 +32,11 @@ export function useAllCommandsAsPreSerializedList( enabled: host !== null && runId != null && options.enabled !== false, } const { cursor, pageLength } = nullCheckedParams + const nullCheckedFixitCommands = params?.includeFixitCommands ?? null + const finalizedNullCheckParams = { + ...nullCheckedParams, + includeFixitCommands: nullCheckedFixitCommands, + } // map undefined values to null to agree with react query caching // TODO (nd: 05/15/2024) create sanitizer for react query key objects @@ -45,12 +50,13 @@ export function useAllCommandsAsPreSerializedList( 'getCommandsAsPreSerializedList', cursor, pageLength, + nullCheckedFixitCommands, ], () => { return getCommandsAsPreSerializedList( host as HostConfig, runId as string, - nullCheckedParams + finalizedNullCheckParams ).then(response => { const responseData = response.data return { diff --git a/react-api-client/src/runs/useAllCommandsQuery.ts b/react-api-client/src/runs/useAllCommandsQuery.ts index 20c598d733f..427e96b554f 100644 --- a/react-api-client/src/runs/useAllCommandsQuery.ts +++ b/react-api-client/src/runs/useAllCommandsQuery.ts @@ -3,20 +3,21 @@ import { getCommands } from '@opentrons/api-client' import { useHost } from '../api' import type { UseQueryOptions, UseQueryResult } from 'react-query' import type { - GetCommandsParams, + GetRunCommandsParamsRequest, HostConfig, CommandsData, } from '@opentrons/api-client' const DEFAULT_PAGE_LENGTH = 30 -export const DEFAULT_PARAMS: GetCommandsParams = { +export const DEFAULT_PARAMS: GetRunCommandsParamsRequest = { cursor: null, pageLength: DEFAULT_PAGE_LENGTH, + includeFixitCommands: null, } export function useAllCommandsQuery( runId: string | null, - params?: GetCommandsParams | null, + params?: GetRunCommandsParamsRequest | null, options: UseQueryOptions = {} ): UseQueryResult { const host = useHost() @@ -27,13 +28,26 @@ export function useAllCommandsQuery( enabled: host !== null && runId != null && options.enabled !== false, } const { cursor, pageLength } = nullCheckedParams + const nullCheckedFixitCommands = params?.includeFixitCommands ?? null + const finalizedNullCheckParams = { + ...nullCheckedParams, + includeFixitCommands: nullCheckedFixitCommands, + } const query = useQuery( - [host, 'runs', runId, 'commands', cursor, pageLength], + [ + host, + 'runs', + runId, + 'commands', + cursor, + pageLength, + finalizedNullCheckParams, + ], () => { return getCommands( host as HostConfig, runId as string, - nullCheckedParams + finalizedNullCheckParams ).then(response => response.data) }, allOptions diff --git a/react-api-client/src/runs/useRunQuery.ts b/react-api-client/src/runs/useRunQuery.ts index 4cb231eb5df..2f0b2fd71e5 100644 --- a/react-api-client/src/runs/useRunQuery.ts +++ b/react-api-client/src/runs/useRunQuery.ts @@ -1,9 +1,11 @@ import { getRun } from '@opentrons/api-client' -import { useQuery } from 'react-query' +import { useQuery, useQueryClient } from 'react-query' import { useHost } from '../api' +import { useEffect } from 'react' +import { some } from 'lodash' +import type { HostConfig, Run, RunError } from '@opentrons/api-client' import type { UseQueryResult, UseQueryOptions } from 'react-query' -import type { HostConfig, Run } from '@opentrons/api-client' export function useRunQuery( runId: string | null, @@ -13,6 +15,7 @@ export function useRunQuery( const contextHost = useHost() const host = hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost + const queryClient = useQueryClient() const query = useQuery( [host, 'runs', runId, 'details'], () => @@ -25,5 +28,31 @@ export function useRunQuery( } ) + const estopInErrorTree = (error: RunError): boolean => + error?.errorCode === '3008' || + some( + (error?.wrappedErrors ?? []).map((wrapped: RunError) => + estopInErrorTree(wrapped) + ) + ) + + // If the run contains an estop error, invalidate the estop query so we get the + // estop modal as fast as we can + useEffect(() => { + if ( + query.data?.data?.current && + some( + ((query.data?.data?.errors ?? []) as RunError[]).map(estopInErrorTree) + ) + ) { + queryClient.invalidateQueries([host, '/robot/control']) + } + }, [ + runId, + query.isSuccess, + query.data?.data?.current, + query.data?.data?.errors, + ]) + return query } diff --git a/robot-server/robot_server/commands/router.py b/robot-server/robot_server/commands/router.py index ef4c58c692c..e4d2d4a9f13 100644 --- a/robot-server/robot_server/commands/router.py +++ b/robot-server/robot_server/commands/router.py @@ -161,7 +161,9 @@ async def get_commands_list( cursor: Cursor index for the collection response. pageLength: Maximum number of items to return. """ - cmd_slice = orchestrator.get_command_slice(cursor=cursor, length=pageLength) + cmd_slice = orchestrator.get_command_slice( + cursor=cursor, length=pageLength, include_fixit_commands=True + ) commands = cast(List[StatelessCommand], cmd_slice.commands) meta = MultiBodyMeta(cursor=cmd_slice.cursor, totalLength=cmd_slice.total_length) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index f315539d469..c1c733a8e12 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -32,6 +32,7 @@ def _build_run( pipettes=[], modules=[], liquids=[], + wells=[], hasEverEnteredErrorRecovery=False, ) return MaintenanceRun.construct( @@ -115,7 +116,9 @@ async def create( state_summary=state_summary, ) - self._maintenance_runs_publisher.publish_current_maintenance_run() + await self._maintenance_runs_publisher.start_publishing_for_maintenance_run( + run_id=run_id, get_state_summary=self._get_state_summary + ) return maintenance_run_data @@ -156,8 +159,7 @@ async def delete(self, run_id: str) -> None: """ if run_id == self._run_orchestrator_store.current_run_id: await self._run_orchestrator_store.clear() - - self._maintenance_runs_publisher.publish_current_maintenance_run() + await self._maintenance_runs_publisher.publish_current_maintenance_run_async() else: raise MaintenanceRunNotFoundError(run_id=run_id) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py b/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py index 169875d4b7d..4aa0c38b323 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py @@ -226,7 +226,9 @@ def get_command_slice( cursor: Requested index of first command in the returned slice. length: Length of slice to return. """ - return self.run_orchestrator.get_command_slice(cursor=cursor, length=length) + return self.run_orchestrator.get_command_slice( + cursor=cursor, length=length, include_fixit_commands=False + ) def get_current_command(self) -> Optional[CommandPointer]: """Get the "current" command, if any.""" diff --git a/robot-server/robot_server/persistence/_migrations/v6_to_v7.py b/robot-server/robot_server/persistence/_migrations/v6_to_v7.py new file mode 100644 index 00000000000..0faee6736e7 --- /dev/null +++ b/robot-server/robot_server/persistence/_migrations/v6_to_v7.py @@ -0,0 +1,84 @@ +"""Migrate the persistence directory from schema 6 to 7. + +Summary of changes from schema 6: + +- Adds a new command_intent to store the commands intent in the commands table +""" + +import json +from pathlib import Path +from contextlib import ExitStack +import shutil +from typing import Any + +import sqlalchemy + +from ..database import sql_engine_ctx, sqlite_rowid +from ..tables import schema_7 +from .._folder_migrator import Migration + +from ..file_and_directory_names import ( + DB_FILE, +) + + +class Migration6to7(Migration): # noqa: D101 + def migrate(self, source_dir: Path, dest_dir: Path) -> None: + """Migrate the persistence directory from schema 6 to 7.""" + # Copy over all existing directories and files to new version + for item in source_dir.iterdir(): + if item.is_dir(): + shutil.copytree(src=item, dst=dest_dir / item.name) + else: + shutil.copy(src=item, dst=dest_dir / item.name) + + dest_db_file = dest_dir / DB_FILE + + # Append the new column to existing protocols in v4 database + with ExitStack() as exit_stack: + dest_engine = exit_stack.enter_context(sql_engine_ctx(dest_db_file)) + + schema_7.metadata.create_all(dest_engine) + + dest_transaction = exit_stack.enter_context(dest_engine.begin()) + + def add_column( + engine: sqlalchemy.engine.Engine, + table_name: str, + column: Any, + ) -> None: + column_type = column.type.compile(engine.dialect) + engine.execute( + f"ALTER TABLE {table_name} ADD COLUMN {column.key} {column_type}" + ) + + add_column( + dest_engine, + schema_7.run_command_table.name, + schema_7.run_command_table.c.command_intent, + ) + + _migrate_command_table_with_new_command_intent_col( + dest_transaction=dest_transaction + ) + + +def _migrate_command_table_with_new_command_intent_col( + dest_transaction: sqlalchemy.engine.Connection, +) -> None: + """Add a new 'command_intent' column to run_command_table table.""" + select_commands = sqlalchemy.select(schema_7.run_command_table).order_by( + sqlite_rowid + ) + for row in dest_transaction.execute(select_commands).all(): + data = json.loads(row.command) + new_command_intent = ( + # Account for old_row.command["intent"] being NULL. + "protocol" + if "intent" not in row.command or data["intent"] == None # noqa: E711 + else data["intent"] + ) + + dest_transaction.execute( + f"UPDATE run_command SET command_intent='{new_command_intent}' WHERE row_id={row.row_id}" + ) diff --git a/robot-server/robot_server/persistence/file_and_directory_names.py b/robot-server/robot_server/persistence/file_and_directory_names.py index 2578ffc21e9..781217f6418 100644 --- a/robot-server/robot_server/persistence/file_and_directory_names.py +++ b/robot-server/robot_server/persistence/file_and_directory_names.py @@ -8,7 +8,7 @@ from typing import Final -LATEST_VERSION_DIRECTORY: Final = "6" +LATEST_VERSION_DIRECTORY: Final = "7" DECK_CONFIGURATION_FILE: Final = "deck_configuration.json" PROTOCOLS_DIRECTORY: Final = "protocols" diff --git a/robot-server/robot_server/persistence/persistence_directory.py b/robot-server/robot_server/persistence/persistence_directory.py index 75acaf46062..c4a9675025a 100644 --- a/robot-server/robot_server/persistence/persistence_directory.py +++ b/robot-server/robot_server/persistence/persistence_directory.py @@ -11,7 +11,7 @@ from anyio import Path as AsyncPath, to_thread from ._folder_migrator import MigrationOrchestrator -from ._migrations import up_to_3, v3_to_v4, v4_to_v5, v5_to_v6 +from ._migrations import up_to_3, v3_to_v4, v4_to_v5, v5_to_v6, v6_to_v7 from .file_and_directory_names import LATEST_VERSION_DIRECTORY _TEMP_PERSISTENCE_DIR_PREFIX: Final = "opentrons-robot-server-" @@ -51,7 +51,8 @@ async def prepare_active_subdirectory(prepared_root: Path) -> Path: up_to_3.MigrationUpTo3(subdirectory="3"), v3_to_v4.Migration3to4(subdirectory="4"), v4_to_v5.Migration4to5(subdirectory="5"), - v5_to_v6.Migration5to6(subdirectory=LATEST_VERSION_DIRECTORY), + v5_to_v6.Migration5to6(subdirectory="6"), + v6_to_v7.Migration6to7(subdirectory=LATEST_VERSION_DIRECTORY), ], temp_file_prefix="temp-", ) diff --git a/robot-server/robot_server/persistence/tables/__init__.py b/robot-server/robot_server/persistence/tables/__init__.py index 3dd235278bf..097383a0612 100644 --- a/robot-server/robot_server/persistence/tables/__init__.py +++ b/robot-server/robot_server/persistence/tables/__init__.py @@ -1,7 +1,7 @@ """SQL database schemas.""" # Re-export the latest schema. -from .schema_6 import ( +from .schema_7 import ( metadata, protocol_table, analysis_table, diff --git a/robot-server/robot_server/persistence/tables/schema_7.py b/robot-server/robot_server/persistence/tables/schema_7.py new file mode 100644 index 00000000000..b08a447505d --- /dev/null +++ b/robot-server/robot_server/persistence/tables/schema_7.py @@ -0,0 +1,265 @@ +"""v6 of our SQLite schema.""" +import enum +import sqlalchemy + +from robot_server.persistence._utc_datetime import UTCDateTime + +metadata = sqlalchemy.MetaData() + + +class PrimitiveParamSQLEnum(enum.Enum): + """Enum type to store primitive param type.""" + + INT = "int" + FLOAT = "float" + BOOL = "bool" + STR = "str" + + +class ProtocolKindSQLEnum(enum.Enum): + """What kind a stored protocol is.""" + + STANDARD = "standard" + QUICK_TRANSFER = "quick-transfer" + + +protocol_table = sqlalchemy.Table( + "protocol", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), + sqlalchemy.Column("protocol_key", sqlalchemy.String, nullable=True), + sqlalchemy.Column( + "protocol_kind", + sqlalchemy.Enum( + ProtocolKindSQLEnum, + values_callable=lambda obj: [e.value for e in obj], + validate_strings=True, + create_constraint=True, + ), + index=True, + nullable=False, + ), +) + +analysis_table = sqlalchemy.Table( + "analysis", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "protocol_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("protocol.id"), + index=True, + nullable=False, + ), + sqlalchemy.Column( + "analyzer_version", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "completed_analysis", + # Stores a JSON string. See CompletedAnalysisStore. + sqlalchemy.String, + nullable=False, + ), +) + +analysis_primitive_type_rtp_table = sqlalchemy.Table( + "analysis_primitive_rtp_table", + metadata, + sqlalchemy.Column( + "row_id", + sqlalchemy.Integer, + primary_key=True, + ), + sqlalchemy.Column( + "analysis_id", + sqlalchemy.ForeignKey("analysis.id"), + nullable=False, + ), + sqlalchemy.Column( + "parameter_variable_name", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "parameter_type", + sqlalchemy.Enum( + PrimitiveParamSQLEnum, + values_callable=lambda obj: [e.value for e in obj], + create_constraint=True, + ), + nullable=False, + ), + sqlalchemy.Column( + "parameter_value", + sqlalchemy.String, + nullable=False, + ), +) + +analysis_csv_rtp_table = sqlalchemy.Table( + "analysis_csv_rtp_table", + metadata, + sqlalchemy.Column( + "row_id", + sqlalchemy.Integer, + primary_key=True, + ), + sqlalchemy.Column( + "analysis_id", + sqlalchemy.ForeignKey("analysis.id"), + nullable=False, + ), + sqlalchemy.Column( + "parameter_variable_name", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "file_id", + sqlalchemy.ForeignKey("data_files.id"), + nullable=True, + ), +) + +run_table = sqlalchemy.Table( + "run", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), + sqlalchemy.Column( + "protocol_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("protocol.id"), + nullable=True, + ), + sqlalchemy.Column( + "state_summary", + sqlalchemy.String, + nullable=True, + ), + sqlalchemy.Column("engine_status", sqlalchemy.String, nullable=True), + sqlalchemy.Column("_updated_at", UTCDateTime, nullable=True), + sqlalchemy.Column( + "run_time_parameters", + # Stores a JSON string. See RunStore. + sqlalchemy.String, + nullable=True, + ), +) + +action_table = sqlalchemy.Table( + "action", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column("created_at", UTCDateTime, nullable=False), + sqlalchemy.Column("action_type", sqlalchemy.String, nullable=False), + sqlalchemy.Column( + "run_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("run.id"), + nullable=False, + ), +) + +run_command_table = sqlalchemy.Table( + "run_command", + metadata, + sqlalchemy.Column("row_id", sqlalchemy.Integer, primary_key=True), + sqlalchemy.Column( + "run_id", sqlalchemy.String, sqlalchemy.ForeignKey("run.id"), nullable=False + ), + sqlalchemy.Column("index_in_run", sqlalchemy.Integer, nullable=False), + sqlalchemy.Column("command_id", sqlalchemy.String, nullable=False), + sqlalchemy.Column("command", sqlalchemy.String, nullable=False), + sqlalchemy.Column("command_intent", sqlalchemy.String, nullable=False, index=True), + sqlalchemy.Index( + "ix_run_run_id_command_id", # An arbitrary name for the index. + "run_id", + "command_id", + unique=True, + ), + sqlalchemy.Index( + "ix_run_run_id_index_in_run", # An arbitrary name for the index. + "run_id", + "index_in_run", + unique=True, + ), +) + +data_files_table = sqlalchemy.Table( + "data_files", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "name", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "file_hash", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), +) + +run_csv_rtp_table = sqlalchemy.Table( + "run_csv_rtp_table", + metadata, + sqlalchemy.Column( + "row_id", + sqlalchemy.Integer, + primary_key=True, + ), + sqlalchemy.Column( + "run_id", + sqlalchemy.ForeignKey("run.id"), + nullable=False, + ), + sqlalchemy.Column( + "parameter_variable_name", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "file_id", + sqlalchemy.ForeignKey("data_files.id"), + nullable=True, + ), +) diff --git a/robot-server/robot_server/runs/router/commands_router.py b/robot-server/robot_server/runs/router/commands_router.py index 56ff466bf84..577606a1446 100644 --- a/robot-server/robot_server/runs/router/commands_router.py +++ b/robot-server/robot_server/runs/router/commands_router.py @@ -285,6 +285,11 @@ async def get_run_commands( description="The maximum number of commands in the list to return.", ), ] = _DEFAULT_COMMAND_LIST_LENGTH, + includeFixitCommands: bool = Query( + True, + description="If `true`, return all commands (protocol, setup, fixit)." + " If `false`, only return safe commands (protocol, setup).", + ), ) -> PydanticResponse[MultiBody[RunCommandSummary, CommandCollectionLinks]]: """Get a summary of a set of commands in a run. @@ -293,12 +298,15 @@ async def get_run_commands( cursor: Cursor index for the collection response. pageLength: Maximum number of items to return. run_data_manager: Run data retrieval interface. + includeFixitCommands: If `true`, return all commands." + " If `false`, only return safe commands. """ try: command_slice = run_data_manager.get_commands_slice( run_id=runId, cursor=cursor, length=pageLength, + include_fixit_commands=includeFixitCommands, ) except RunNotFoundError as e: raise RunNotFound.from_exc(e).as_error(status.HTTP_404_NOT_FOUND) from e @@ -368,15 +376,24 @@ async def get_run_commands( async def get_run_commands_as_pre_serialized_list( runId: str, run_data_manager: Annotated[RunDataManager, Depends(get_run_data_manager)], + includeFixitCommands: bool = Query( + True, + description="If `true`, return all commands (protocol, setup, fixit)." + " If `false`, only return safe commands (protocol, setup).", + ), ) -> PydanticResponse[SimpleMultiBody[str]]: """Get all commands of a completed run as a list of pre-serialized (string encoded) commands. Arguments: runId: Requested run ID, from the URL run_data_manager: Run data retrieval interface. + includeFixitCommands: If `true`, return all commands." + " If `false`, only return safe commands. """ try: - commands = run_data_manager.get_all_commands_as_preserialized_list(runId) + commands = run_data_manager.get_all_commands_as_preserialized_list( + run_id=runId, include_fixit_commands=includeFixitCommands + ) except RunNotFoundError as e: raise RunNotFound.from_exc(e).as_error(status.HTTP_404_NOT_FOUND) from e except PreSerializedCommandsNotAvailableError as e: diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index f8ef3fd1d8d..c8d0db1b0d3 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -71,6 +71,7 @@ def _build_run( pipettes=[], modules=[], liquids=[], + wells=[], hasEverEnteredErrorRecovery=False, ) errors.append(state_summary.dataError) @@ -371,6 +372,7 @@ def get_commands_slice( run_id: str, cursor: Optional[int], length: int, + include_fixit_commands: bool, ) -> CommandSlice: """Get a slice of run commands. @@ -378,18 +380,21 @@ def get_commands_slice( run_id: ID of the run. cursor: Requested index of first command in the returned slice. length: Length of slice to return. + include_fixit_commands: Include fixit commands. Raises: RunNotFoundError: The given run identifier was not found in the database. """ if run_id == self._run_orchestrator_store.current_run_id: return self._run_orchestrator_store.get_command_slice( - cursor=cursor, length=length + cursor=cursor, + length=length, + include_fixit_commands=include_fixit_commands, ) # Let exception propagate return self._run_store.get_commands_slice( - run_id=run_id, cursor=cursor, length=length + run_id=run_id, cursor=cursor, length=length, include_fixit_commands=True ) def get_command_error_slice( @@ -465,10 +470,12 @@ def get_command_errors(self, run_id: str) -> list[ErrorOccurrence]: if run_id == self._run_orchestrator_store.current_run_id: return self._run_orchestrator_store.get_command_errors() - # TODO(tz, 8-5-2024): Change this to return to error list from the DB when we implement https://opentrons.atlassian.net/browse/EXEC-655. + # TODO(tz, 8-5-2024): Change this to return the error list from the DB when we implement https://opentrons.atlassian.net/browse/EXEC-655. raise RunNotCurrentError() - def get_all_commands_as_preserialized_list(self, run_id: str) -> List[str]: + def get_all_commands_as_preserialized_list( + self, run_id: str, include_fixit_commands: bool + ) -> List[str]: """Get all commands of a run in a serialized json list.""" if ( run_id == self._run_orchestrator_store.current_run_id @@ -477,7 +484,9 @@ def get_all_commands_as_preserialized_list(self, run_id: str) -> List[str]: raise PreSerializedCommandsNotAvailableError( "Pre-serialized commands are only available after a run has ended." ) - return self._run_store.get_all_commands_as_preserialized_list(run_id) + return self._run_store.get_all_commands_as_preserialized_list( + run_id, include_fixit_commands + ) def set_policies(self, run_id: str, policies: List[ErrorRecoveryRule]) -> None: """Create run policy rules for error recovery.""" diff --git a/robot-server/robot_server/runs/run_orchestrator_store.py b/robot-server/robot_server/runs/run_orchestrator_store.py index 72fb80d1ef2..8b52430a12e 100644 --- a/robot-server/robot_server/runs/run_orchestrator_store.py +++ b/robot-server/robot_server/runs/run_orchestrator_store.py @@ -330,17 +330,18 @@ def get_current_command(self) -> Optional[CommandPointer]: return self.run_orchestrator.get_current_command() def get_command_slice( - self, - cursor: Optional[int], - length: int, + self, cursor: Optional[int], length: int, include_fixit_commands: bool ) -> CommandSlice: """Get a slice of run commands. Args: cursor: Requested index of first command in the returned slice. length: Length of slice to return. + include_fixit_commands: Include fixit commands. """ - return self.run_orchestrator.get_command_slice(cursor=cursor, length=length) + return self.run_orchestrator.get_command_slice( + cursor=cursor, length=length, include_fixit_commands=include_fixit_commands + ) def get_command_error_slice( self, diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 0de7d08bac6..6ab8665c454 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -7,10 +7,11 @@ from typing import Dict, List, Optional, Literal, Union import sqlalchemy +from sqlalchemy import and_ from pydantic import ValidationError from opentrons.util.helpers import utc_now -from opentrons.protocol_engine import StateSummary, CommandSlice +from opentrons.protocol_engine import StateSummary, CommandSlice, CommandIntent from opentrons.protocol_engine.commands import Command from opentrons.protocol_engine.types import RunTimeParameter @@ -175,6 +176,9 @@ def update_run_state( "index_in_run": command_index, "command_id": command.id, "command": pydantic_to_json(command), + "command_intent": str(command.intent.value) + if command.intent + else CommandIntent.PROTOCOL, }, ) @@ -430,6 +434,7 @@ def get_commands_slice( run_id: str, length: int, cursor: Optional[int], + include_fixit_commands: bool, ) -> CommandSlice: """Get a slice of run commands from the store. @@ -439,6 +444,7 @@ def get_commands_slice( cursor: The starting index of the slice in the whole collection. If `None`, up to `length` elements at the end of the collection will be returned. + include_fixit_commands: Wether we should include fixit command intent in the result. Returns: A collection of commands as well as the actual cursor used and @@ -451,26 +457,47 @@ def get_commands_slice( if not self._run_exists(run_id, transaction): raise RunNotFoundError(run_id=run_id) - select_count = sqlalchemy.select(sqlalchemy.func.count()).where( - run_command_table.c.run_id == run_id - ) + if include_fixit_commands: + select_count = sqlalchemy.select(sqlalchemy.func.count()).where( + run_command_table.c.run_id == run_id + ) + else: + select_count = sqlalchemy.select(sqlalchemy.func.count()).where( + and_( + run_command_table.c.run_id == run_id, + run_command_table.c.command_intent != "fixit", + ) + ) count_result: int = transaction.execute(select_count).scalar_one() actual_cursor = cursor if cursor is not None else count_result - length # Clamp to [0, count_result). actual_cursor = max(0, min(actual_cursor, count_result - 1)) - - select_slice = ( - sqlalchemy.select( - run_command_table.c.index_in_run, run_command_table.c.command + if include_fixit_commands: + select_slice = ( + sqlalchemy.select( + run_command_table.c.index_in_run, run_command_table.c.command + ) + .where( + run_command_table.c.run_id == run_id, + run_command_table.c.index_in_run >= actual_cursor, + run_command_table.c.index_in_run < actual_cursor + length, + ) + .order_by(run_command_table.c.index_in_run) ) - .where( - run_command_table.c.run_id == run_id, - run_command_table.c.index_in_run >= actual_cursor, - run_command_table.c.index_in_run < actual_cursor + length, + else: + select_slice = ( + sqlalchemy.select( + run_command_table.c.index_in_run, run_command_table.c.command + ) + .where( + run_command_table.c.run_id == run_id, + run_command_table.c.index_in_run >= actual_cursor, + run_command_table.c.index_in_run < actual_cursor + length, + run_command_table.c.command_intent != "fixit", + ) + .order_by(run_command_table.c.index_in_run) ) - .order_by(run_command_table.c.index_in_run) - ) slice_result = transaction.execute(select_slice).all() sliced_commands: List[Command] = [ @@ -484,16 +511,29 @@ def get_commands_slice( commands=sliced_commands, ) - def get_all_commands_as_preserialized_list(self, run_id: str) -> List[str]: + def get_all_commands_as_preserialized_list( + self, run_id: str, include_fixit_commands: bool + ) -> List[str]: """Get all commands of the run as a list of strings of json command objects.""" with self._sql_engine.begin() as transaction: if not self._run_exists(run_id, transaction): raise RunNotFoundError(run_id=run_id) - select_commands = ( - sqlalchemy.select(run_command_table.c.command) - .where(run_command_table.c.run_id == run_id) - .order_by(run_command_table.c.index_in_run) - ) + # TODO (tz, 8-21-24): consolidate into 1 query. + if include_fixit_commands: + select_commands = ( + sqlalchemy.select(run_command_table.c.command) + .where(run_command_table.c.run_id == run_id) + .order_by(run_command_table.c.index_in_run) + ) + else: + select_commands = ( + sqlalchemy.select(run_command_table.c.command) + .where( + and_(run_command_table.c.run_id == run_id), + run_command_table.c.command_intent != "fixit", + ) + .order_by(run_command_table.c.index_in_run) + ) commands_result = transaction.scalars(select_commands).all() return commands_result diff --git a/robot-server/robot_server/service/legacy/routers/modules.py b/robot-server/robot_server/service/legacy/routers/modules.py index 69fa07ecb38..50fd93d8cfd 100644 --- a/robot-server/robot_server/service/legacy/routers/modules.py +++ b/robot-server/robot_server/service/legacy/routers/modules.py @@ -86,9 +86,9 @@ async def post_serial_command( if requested_version >= 3: raise LegacyErrorResponse.from_exc( APIRemoved( - "/modules/{serial}", - "3", - "This endpoint has been removed. Use POST /commands instead.", + api_element="/modules/{serial}", + since_version="3", + extra_message="This endpoint has been removed. Use POST /commands instead.", ), ).as_error(status.HTTP_410_GONE) diff --git a/robot-server/robot_server/service/legacy/routers/networking.py b/robot-server/robot_server/service/legacy/routers/networking.py index 17f4a3364cc..b47cf283ddf 100644 --- a/robot-server/robot_server/service/legacy/routers/networking.py +++ b/robot-server/robot_server/service/legacy/routers/networking.py @@ -1,10 +1,10 @@ import logging import os import subprocess +from typing import Annotated, Optional from starlette import status from starlette.responses import JSONResponse -from typing import Annotated, Optional from fastapi import APIRouter, HTTPException, File, Path, UploadFile, Query from opentrons_shared_data.errors import ErrorCodes @@ -45,8 +45,20 @@ async def get_networking_status() -> NetworkingStatus: try: connectivity = await nmcli.is_connected() - # TODO(mc, 2020-09-17): interfaces should be typed - interfaces = {i.value: await nmcli.iface_info(i) for i in nmcli.NETWORK_IFACES} + + async def _permissive_get_iface( + i: nmcli.NETWORK_IFACES, + ) -> dict[str, dict[str, str | None]]: + try: + return {i.value: await nmcli.iface_info(i)} + except ValueError: + log.warning(f"Could not get state of iface {i.value}") + return {} + + interfaces: dict[str, dict[str, str | None]] = {} + for interface in nmcli.NETWORK_IFACES: + this_iface = await _permissive_get_iface(interface) + interfaces.update(this_iface) log.debug(f"Connectivity: {connectivity}") log.debug(f"Interfaces: {interfaces}") return NetworkingStatus( diff --git a/robot-server/robot_server/service/notifications/publisher_notifier.py b/robot-server/robot_server/service/notifications/publisher_notifier.py index f96ae9c3f96..89a53e27b64 100644 --- a/robot-server/robot_server/service/notifications/publisher_notifier.py +++ b/robot-server/robot_server/service/notifications/publisher_notifier.py @@ -1,5 +1,6 @@ """Provides an interface for alerting notification publishers to events and related lifecycle utilities.""" import asyncio +from logging import getLogger from fastapi import Depends from typing import Annotated, Optional, Callable, List, Awaitable, Union @@ -11,6 +12,8 @@ from opentrons.util.change_notifier import ChangeNotifier, ChangeNotifier_ts +LOG = getLogger(__name__) + class PublisherNotifier: """An interface that invokes notification callbacks whenever a generic notify event occurs.""" @@ -28,10 +31,9 @@ def register_publish_callbacks( def _initialize(self) -> None: """Initializes an instance of PublisherNotifier. This method should only be called once.""" - # fixme(mm, 2024-08-20): This task currently leaks; this class needs a close() - # method or something. This gets easier when app_setup.py switches to using a - # context manager for ASGI app setup and teardown. - self._notifier = asyncio.create_task(self._wait_for_event()) + self._notifier = asyncio.create_task( + self._wait_for_event(), name="Run publisher notifier" + ) def _notify_publishers(self) -> None: """A generic notifier, alerting all `waiters` of a change.""" @@ -39,10 +41,18 @@ def _notify_publishers(self) -> None: async def _wait_for_event(self) -> None: """Indefinitely wait for an event to occur, then invoke each callback.""" - while True: - await self._change_notifier.wait() - for callback in self._callbacks: - await callback() + try: + while True: + await self._change_notifier.wait() + for callback in self._callbacks: + try: + await callback() + except BaseException: + LOG.exception( + f'PublisherNotifier: exception in callback {getattr(callback, "__name__", "")}' + ) + except BaseException: + LOG.exception("PublisherNotifer notify task failed") _pe_publisher_notifier_accessor: AppStateAccessor[PublisherNotifier] = AppStateAccessor[ diff --git a/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py index 8e322d63f36..eea7f35abe8 100644 --- a/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py @@ -1,22 +1,74 @@ -from typing import Annotated - +from typing import Annotated, Callable, Optional +from dataclasses import dataclass from fastapi import Depends +from opentrons.protocol_engine.state.state_summary import StateSummary +from opentrons.protocol_engine.types import EngineStatus from server_utils.fastapi_utils.app_state import ( AppState, AppStateAccessor, get_app_state, ) from ..notification_client import NotificationClient, get_notification_client +from ..publisher_notifier import PublisherNotifier, get_pe_publisher_notifier from .. import topics +@dataclass +class _RunHooks: + """Generated during a protocol run. Utilized by MaintenanceRunsPublisher.""" + + run_id: str + get_state_summary: Callable[[str], Optional[StateSummary]] + + +@dataclass +class _EngineStateSlice: + """Protocol Engine state relevant to MaintenanceRunsPublisher.""" + + state_summary_status: Optional[EngineStatus] = None + + class MaintenanceRunsPublisher: """Publishes maintenance run topics.""" - def __init__(self, client: NotificationClient) -> None: + def __init__( + self, client: NotificationClient, publisher_notifier: PublisherNotifier + ) -> None: """Returns a configured Maintenance Runs Publisher.""" self._client = client + self._run_hooks: Optional[_RunHooks] = None + self._engine_state_slice: Optional[_EngineStateSlice] = None + publisher_notifier.register_publish_callbacks( + [ + self._handle_engine_status_change, + ] + ) + + async def start_publishing_for_maintenance_run( + self, + run_id: str, + get_state_summary: Callable[[str], Optional[StateSummary]], + ) -> None: + """Initialize RunsPublisher with necessary information derived from the current run. + + Args: + run_id: ID of the current run. + get_state_summary: Callback to get the current run's state summary, if any. + """ + self._run_hooks = _RunHooks( + run_id=run_id, + get_state_summary=get_state_summary, + ) + self._engine_state_slice = _EngineStateSlice() + + await self.publish_current_maintenance_run_async() + + async def publish_current_maintenance_run_async( + self, + ) -> None: + """Publishes the equivalent of GET /maintenance_run/current_run""" + self._client.publish_advise_refetch(topic=topics.MAINTENANCE_RUNS_CURRENT_RUN) def publish_current_maintenance_run( self, @@ -24,6 +76,21 @@ def publish_current_maintenance_run( """Publishes the equivalent of GET /maintenance_run/current_run""" self._client.publish_advise_refetch(topic=topics.MAINTENANCE_RUNS_CURRENT_RUN) + async def _handle_engine_status_change(self) -> None: + """Publish a refetch flag if the engine status has changed.""" + if self._run_hooks is not None and self._engine_state_slice is not None: + new_state_summary = self._run_hooks.get_state_summary( + self._run_hooks.run_id + ) + + if ( + new_state_summary is not None + and self._engine_state_slice.state_summary_status + != new_state_summary.status + ): + await self.publish_current_maintenance_run_async() + self._engine_state_slice.state_summary_status = new_state_summary.status + _maintenance_runs_publisher_accessor: AppStateAccessor[ MaintenanceRunsPublisher @@ -35,6 +102,9 @@ async def get_maintenance_runs_publisher( notification_client: Annotated[ NotificationClient, Depends(get_notification_client) ], + publisher_notifier: Annotated[ + PublisherNotifier, Depends(get_pe_publisher_notifier) + ], ) -> MaintenanceRunsPublisher: """Get a singleton MaintenanceRunsPublisher to publish maintenance run topics.""" maintenance_runs_publisher = _maintenance_runs_publisher_accessor.get_from( @@ -43,7 +113,7 @@ async def get_maintenance_runs_publisher( if maintenance_runs_publisher is None: maintenance_runs_publisher = MaintenanceRunsPublisher( - client=notification_client + client=notification_client, publisher_notifier=publisher_notifier ) _maintenance_runs_publisher_accessor.set_on( app_state, maintenance_runs_publisher diff --git a/robot-server/tests/commands/test_router.py b/robot-server/tests/commands/test_router.py index 259af673fe9..4ccc1a1acc1 100644 --- a/robot-server/tests/commands/test_router.py +++ b/robot-server/tests/commands/test_router.py @@ -137,7 +137,11 @@ async def test_get_commands_list( index=0, ) ) - decoy.when(run_orchestrator.get_command_slice(cursor=1337, length=42)).then_return( + decoy.when( + run_orchestrator.get_command_slice( + cursor=1337, length=42, include_fixit_commands=True + ) + ).then_return( CommandSlice(commands=[command_1, command_2], cursor=0, total_length=2) ) diff --git a/robot-server/tests/integration/http_api/persistence/test_reset.py b/robot-server/tests/integration/http_api/persistence/test_reset.py index 937d17c81f0..8b4ad48ad19 100644 --- a/robot-server/tests/integration/http_api/persistence/test_reset.py +++ b/robot-server/tests/integration/http_api/persistence/test_reset.py @@ -40,9 +40,9 @@ async def _assert_reset_was_successful( all_files_and_directories = set(persistence_directory.glob("**/*")) expected_files_and_directories = { persistence_directory / "robot_server.db", - persistence_directory / "6", - persistence_directory / "6" / "protocols", - persistence_directory / "6" / "robot_server.db", + persistence_directory / "7", + persistence_directory / "7" / "protocols", + persistence_directory / "7" / "robot_server.db", } assert all_files_and_directories == expected_files_and_directories diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index 7baffe86a29..a4431f7b463 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -69,6 +69,7 @@ def engine_state_summary() -> StateSummary: pipettes=[LoadedPipette.construct(id="some-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="some-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + wells=[], ) diff --git a/robot-server/tests/persistence/test_tables.py b/robot-server/tests/persistence/test_tables.py index 5b38cf1c616..757cdd9a570 100644 --- a/robot-server/tests/persistence/test_tables.py +++ b/robot-server/tests/persistence/test_tables.py @@ -13,6 +13,7 @@ schema_4, schema_5, schema_6, + schema_7, ) # The statements that we expect to emit when we create a fresh database. @@ -28,6 +29,122 @@ # # Whitespace and formatting changes, on the other hand, are allowed. EXPECTED_STATEMENTS_LATEST = [ + """ + CREATE TABLE protocol ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + protocol_key VARCHAR, + protocol_kind VARCHAR(14) NOT NULL, + PRIMARY KEY (id), + CONSTRAINT protocolkindsqlenum CHECK (protocol_kind IN ('standard', 'quick-transfer')) + ) + """, + """ + CREATE TABLE analysis ( + id VARCHAR NOT NULL, + protocol_id VARCHAR NOT NULL, + analyzer_version VARCHAR NOT NULL, + completed_analysis VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(protocol_id) REFERENCES protocol (id) + ) + """, + """ + CREATE TABLE analysis_primitive_rtp_table ( + row_id INTEGER NOT NULL, + analysis_id VARCHAR NOT NULL, + parameter_variable_name VARCHAR NOT NULL, + parameter_type VARCHAR(5) NOT NULL, + parameter_value VARCHAR NOT NULL, + PRIMARY KEY (row_id), + FOREIGN KEY(analysis_id) REFERENCES analysis (id), + CONSTRAINT primitiveparamsqlenum CHECK (parameter_type IN ('int', 'float', 'bool', 'str')) + ) + """, + """ + CREATE TABLE analysis_csv_rtp_table ( + row_id INTEGER NOT NULL, + analysis_id VARCHAR NOT NULL, + parameter_variable_name VARCHAR NOT NULL, + file_id VARCHAR, + PRIMARY KEY (row_id), + FOREIGN KEY(analysis_id) REFERENCES analysis (id), + FOREIGN KEY(file_id) REFERENCES data_files (id) + ) + """, + """ + CREATE INDEX ix_analysis_protocol_id ON analysis (protocol_id) + """, + """ + CREATE TABLE run ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + protocol_id VARCHAR, + state_summary VARCHAR, + engine_status VARCHAR, + _updated_at DATETIME, + run_time_parameters VARCHAR, + PRIMARY KEY (id), + FOREIGN KEY(protocol_id) REFERENCES protocol (id) + ) + """, + """ + CREATE TABLE action ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + action_type VARCHAR NOT NULL, + run_id VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(run_id) REFERENCES run (id) + ) + """, + """ + CREATE TABLE run_command ( + row_id INTEGER NOT NULL, + run_id VARCHAR NOT NULL, + index_in_run INTEGER NOT NULL, + command_id VARCHAR NOT NULL, + command VARCHAR NOT NULL, + command_intent VARCHAR NOT NULL, + PRIMARY KEY (row_id), + FOREIGN KEY(run_id) REFERENCES run (id) + ) + """, + """ + CREATE UNIQUE INDEX ix_run_run_id_command_id ON run_command (run_id, command_id) + """, + """ + CREATE UNIQUE INDEX ix_run_run_id_index_in_run ON run_command (run_id, index_in_run) + """, + """ + CREATE INDEX ix_protocol_protocol_kind ON protocol (protocol_kind) + """, + """ + CREATE INDEX ix_run_command_command_intent ON run_command (command_intent) + """, + """ + CREATE TABLE data_files ( + id VARCHAR NOT NULL, + name VARCHAR NOT NULL, + file_hash VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + PRIMARY KEY (id) + ) + """, + """ + CREATE TABLE run_csv_rtp_table ( + row_id INTEGER NOT NULL, + run_id VARCHAR NOT NULL, + parameter_variable_name VARCHAR NOT NULL, + file_id VARCHAR, + PRIMARY KEY (row_id), + FOREIGN KEY(run_id) REFERENCES run (id), + FOREIGN KEY(file_id) REFERENCES data_files (id) + ) + """, +] + +EXPECTED_STATEMENTS_V6 = [ """ CREATE TABLE protocol ( id VARCHAR NOT NULL, @@ -139,7 +256,8 @@ """, ] -EXPECTED_STATEMENTS_V6 = EXPECTED_STATEMENTS_LATEST + +EXPECTED_STATEMENTS_V7 = EXPECTED_STATEMENTS_LATEST EXPECTED_STATEMENTS_V5 = [ """ @@ -420,6 +538,7 @@ def _normalize_statement(statement: str) -> str: ("metadata", "expected_statements"), [ (latest_metadata, EXPECTED_STATEMENTS_LATEST), + (schema_7.metadata, EXPECTED_STATEMENTS_V7), (schema_6.metadata, EXPECTED_STATEMENTS_V6), (schema_5.metadata, EXPECTED_STATEMENTS_V5), (schema_4.metadata, EXPECTED_STATEMENTS_V4), diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index 87108ff75f8..8448ded8870 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -189,6 +189,7 @@ async def test_analyze( modules=[], labwareOffsets=[], liquids=[], + wells=[], hasEverEnteredErrorRecovery=False, ), parameters=[bool_parameter], diff --git a/robot-server/tests/runs/router/test_commands_router.py b/robot-server/tests/runs/router/test_commands_router.py index 38a09706826..e7045fe6287 100644 --- a/robot-server/tests/runs/router/test_commands_router.py +++ b/robot-server/tests/runs/router/test_commands_router.py @@ -345,9 +345,7 @@ async def test_get_run_commands( ) decoy.when( mock_run_data_manager.get_commands_slice( - run_id="run-id", - cursor=None, - length=42, + run_id="run-id", cursor=None, length=42, include_fixit_commands=True ) ).then_return(CommandSlice(commands=[command], cursor=1, total_length=3)) @@ -356,6 +354,7 @@ async def test_get_run_commands( run_data_manager=mock_run_data_manager, cursor=None, pageLength=42, + includeFixitCommands=True, ) assert result.content.data == [ @@ -412,7 +411,9 @@ async def test_get_run_commands_empty( """It should return an empty commands list if no commands.""" decoy.when(mock_run_data_manager.get_current_command("run-id")).then_return(None) decoy.when( - mock_run_data_manager.get_commands_slice(run_id="run-id", cursor=21, length=42) + mock_run_data_manager.get_commands_slice( + run_id="run-id", cursor=21, length=42, include_fixit_commands=True + ) ).then_return(CommandSlice(commands=[], cursor=0, total_length=0)) result = await get_run_commands( @@ -420,6 +421,7 @@ async def test_get_run_commands_empty( run_data_manager=mock_run_data_manager, cursor=21, pageLength=42, + includeFixitCommands=True, ) assert result.content.data == [] @@ -436,11 +438,10 @@ async def test_get_run_commands_not_found( not_found_error = RunNotFoundError("oh no") decoy.when( - mock_run_data_manager.get_commands_slice(run_id="run-id", cursor=21, length=42) + mock_run_data_manager.get_commands_slice( + run_id="run-id", cursor=21, length=42, include_fixit_commands=True + ) ).then_raise(not_found_error) - decoy.when(mock_run_data_manager.get_current_command(run_id="run-id")).then_raise( - not_found_error - ) with pytest.raises(ApiError) as exc_info: await get_run_commands( @@ -448,6 +449,7 @@ async def test_get_run_commands_not_found( run_data_manager=mock_run_data_manager, cursor=21, pageLength=42, + includeFixitCommands=True, ) assert exc_info.value.status_code == 404 diff --git a/robot-server/tests/runs/test_error_recovery_mapping.py b/robot-server/tests/runs/test_error_recovery_mapping.py index 21195872d39..a142fbc5e30 100644 --- a/robot-server/tests/runs/test_error_recovery_mapping.py +++ b/robot-server/tests/runs/test_error_recovery_mapping.py @@ -3,10 +3,7 @@ from decoy import Decoy -from opentrons.protocol_engine.commands.pipetting_common import ( - LiquidNotFoundError, - LiquidNotFoundErrorInternalData, -) +from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError from opentrons.protocol_engine.commands.command import ( DefinedErrorData, ) @@ -38,9 +35,7 @@ def mock_command(decoy: Decoy) -> LiquidProbe: @pytest.fixture def mock_error_data(decoy: Decoy) -> CommandDefinedErrorData: """Get a mock TipPhysicallyMissingError.""" - mock = decoy.mock( - cls=DefinedErrorData[LiquidNotFoundError, LiquidNotFoundErrorInternalData] - ) + mock = decoy.mock(cls=DefinedErrorData[LiquidNotFoundError]) mock_lnfe = decoy.mock(cls=LiquidNotFoundError) decoy.when(mock.public).then_return(mock_lnfe) decoy.when(mock_lnfe.errorType).then_return("liquidNotFound") diff --git a/robot-server/tests/runs/test_run_controller.py b/robot-server/tests/runs/test_run_controller.py index a6e0f5e1b25..d60e9da6082 100644 --- a/robot-server/tests/runs/test_run_controller.py +++ b/robot-server/tests/runs/test_run_controller.py @@ -71,6 +71,7 @@ def engine_state_summary() -> StateSummary: pipettes=[], modules=[], liquids=[], + wells=[], hasEverEnteredErrorRecovery=False, ) @@ -209,6 +210,24 @@ async def test_create_play_action_to_start( times=1, ) + # Verify maintenance run publication after background task execution + decoy.verify( + mock_maintenance_runs_publisher.publish_current_maintenance_run(), + times=1, + ) + + # Verify maintenance run publication after background task execution + decoy.verify( + mock_maintenance_runs_publisher.publish_current_maintenance_run(), + times=1, + ) + + # Verify maintenance run publication after background task execution + decoy.verify( + mock_maintenance_runs_publisher.publish_current_maintenance_run(), + times=1, + ) + def test_create_pause_action( decoy: Decoy, diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 1d67ff295fb..801f09f080f 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -96,6 +96,7 @@ def engine_state_summary() -> StateSummary: pipettes=[LoadedPipette.construct(id="some-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="some-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + wells=[], ) @@ -493,6 +494,7 @@ async def test_get_all_runs( pipettes=[LoadedPipette.construct(id="current-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="current-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + wells=[], ) current_run_time_parameters: List[pe_types.RunTimeParameter] = [ pe_types.BooleanParameter( @@ -512,6 +514,7 @@ async def test_get_all_runs( pipettes=[LoadedPipette.construct(id="old-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="old-module-id")], # type: ignore[call-arg] liquids=[], + wells=[], ) historical_run_time_parameters: List[pe_types.RunTimeParameter] = [ pe_types.BooleanParameter( @@ -668,6 +671,10 @@ async def test_update_current( mock_runs_publisher.publish_runs_advise_refetch(run_id), times=1, ) + decoy.verify( + mock_runs_publisher.publish_runs_advise_refetch(run_id), + times=1, + ) assert result == Run( current=False, id=run_resource.run_id, @@ -846,9 +853,13 @@ def test_get_commands_slice_from_db( ) decoy.when( - mock_run_store.get_commands_slice(run_id="run_id", cursor=1, length=2) + mock_run_store.get_commands_slice( + run_id="run_id", cursor=1, length=2, include_fixit_commands=True + ) ).then_return(expected_command_slice) - result = subject.get_commands_slice(run_id="run_id", cursor=1, length=2) + result = subject.get_commands_slice( + run_id="run_id", cursor=1, length=2, include_fixit_commands=True + ) assert expected_command_slice == result @@ -875,11 +886,11 @@ def test_get_commands_slice_current_run( commands=expected_commands_result, cursor=1, total_length=3 ) decoy.when(mock_run_orchestrator_store.current_run_id).then_return("run-id") - decoy.when(mock_run_orchestrator_store.get_command_slice(1, 2)).then_return( + decoy.when(mock_run_orchestrator_store.get_command_slice(1, 2, True)).then_return( expected_command_slice ) - result = subject.get_commands_slice("run-id", 1, 2) + result = subject.get_commands_slice("run-id", 1, 2, include_fixit_commands=True) assert expected_command_slice == result @@ -926,10 +937,14 @@ def test_get_commands_slice_from_db_run_not_found( ) -> None: """Should get a sliced command list from run store.""" decoy.when( - mock_run_store.get_commands_slice(run_id="run-id", cursor=1, length=2) + mock_run_store.get_commands_slice( + run_id="run-id", cursor=1, length=2, include_fixit_commands=True + ) ).then_raise(RunNotFoundError(run_id="run-id")) with pytest.raises(RunNotFoundError): - subject.get_commands_slice(run_id="run-id", cursor=1, length=2) + subject.get_commands_slice( + run_id="run-id", cursor=1, length=2, include_fixit_commands=True + ) def test_get_current_command( @@ -1045,9 +1060,9 @@ def test_get_all_commands_as_preserialized_list( """It should return the pre-serialized commands list.""" decoy.when(mock_run_orchestrator_store.current_run_id).then_return(None) decoy.when( - mock_run_store.get_all_commands_as_preserialized_list("run-id") + mock_run_store.get_all_commands_as_preserialized_list("run-id", True) ).then_return(['{"id": command-1}', '{"id": command-2}']) - assert subject.get_all_commands_as_preserialized_list("run-id") == [ + assert subject.get_all_commands_as_preserialized_list("run-id", True) == [ '{"id": command-1}', '{"id": command-2}', ] @@ -1063,7 +1078,7 @@ def test_get_all_commands_as_preserialized_list_errors_for_active_runs( decoy.when(mock_run_orchestrator_store.current_run_id).then_return("current-run-id") decoy.when(mock_run_orchestrator_store.get_is_run_terminal()).then_return(False) with pytest.raises(PreSerializedCommandsNotAvailableError): - subject.get_all_commands_as_preserialized_list("current-run-id") + subject.get_all_commands_as_preserialized_list("current-run-id", True) async def test_get_current_run_labware_definition( diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index 74dcffac14f..55a1849e693 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -64,6 +64,7 @@ def protocol_commands() -> List[pe_commands.Command]: createdAt=datetime(year=2021, month=1, day=1), params=pe_commands.WaitForResumeParams(message="hello world"), result=pe_commands.WaitForResumeResult(), + intent=pe_commands.CommandIntent.PROTOCOL, ), pe_commands.WaitForResume( id="pause-2", @@ -72,6 +73,7 @@ def protocol_commands() -> List[pe_commands.Command]: createdAt=datetime(year=2022, month=2, day=2), params=pe_commands.WaitForResumeParams(message="hey world"), result=pe_commands.WaitForResumeResult(), + intent=pe_commands.CommandIntent.PROTOCOL, ), pe_commands.WaitForResume( id="pause-3", @@ -81,6 +83,15 @@ def protocol_commands() -> List[pe_commands.Command]: params=pe_commands.WaitForResumeParams(message="sup world"), result=pe_commands.WaitForResumeResult(), ), + pe_commands.WaitForResume( + id="fixit-pause-1", + key="command-key", + status=pe_commands.CommandStatus.SUCCEEDED, + createdAt=datetime(year=2021, month=1, day=1), + params=pe_commands.WaitForResumeParams(message="hello world"), + result=pe_commands.WaitForResumeResult(), + intent=pe_commands.CommandIntent.FIXIT, + ), ] @@ -120,6 +131,7 @@ def state_summary() -> StateSummary: labwareOffsets=[], status=EngineStatus.IDLE, liquids=liquids, + wells=[], hasEverEnteredErrorRecovery=False, ) @@ -203,6 +215,7 @@ def invalid_state_summary() -> StateSummary: labwareOffsets=[], status=EngineStatus.IDLE, liquids=liquids, + wells=[], ) @@ -252,6 +265,7 @@ async def test_update_run_state( run_id="run-id", length=len(protocol_commands), cursor=0, + include_fixit_commands=True, ) assert result == RunResource( @@ -729,7 +743,10 @@ def test_get_command_slice( run_time_parameters=[], ) result = subject.get_commands_slice( - run_id="run-id", cursor=0, length=len(protocol_commands) + run_id="run-id", + cursor=0, + length=len(protocol_commands), + include_fixit_commands=True, ) assert result == CommandSlice( @@ -743,15 +760,15 @@ def test_get_command_slice( ("input_cursor", "input_length", "expected_cursor", "expected_command_ids"), [ (0, 0, 0, []), - (None, 0, 2, []), + (None, 0, 3, []), (0, 3, 0, ["pause-1", "pause-2", "pause-3"]), (0, 1, 0, ["pause-1"]), (1, 2, 1, ["pause-2", "pause-3"]), - (0, 999, 0, ["pause-1", "pause-2", "pause-3"]), - (1, 999, 1, ["pause-2", "pause-3"]), - (None, 3, 0, ["pause-1", "pause-2", "pause-3"]), - (None, 2, 1, ["pause-2", "pause-3"]), - (999, 2, 2, ["pause-3"]), + (0, 999, 0, ["pause-1", "pause-2", "pause-3", "fixit-pause-1"]), + (1, 999, 1, ["pause-2", "pause-3", "fixit-pause-1"]), + (None, 3, 1, ["pause-2", "pause-3", "fixit-pause-1"]), + (None, 2, 2, ["pause-3", "fixit-pause-1"]), + (999, 2, 3, ["fixit-pause-1"]), ], ) def test_get_commands_slice_clamping( @@ -776,7 +793,10 @@ def test_get_commands_slice_clamping( run_time_parameters=[], ) result = subject.get_commands_slice( - run_id="run-id", cursor=input_cursor, length=input_length + run_id="run-id", + cursor=input_cursor, + length=input_length, + include_fixit_commands=True, ) assert result.cursor == expected_cursor @@ -794,7 +814,9 @@ def test_get_run_command_slice_none(subject: RunStore) -> None: created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), ) - result = subject.get_commands_slice(run_id="run-id", length=999, cursor=None) + result = subject.get_commands_slice( + run_id="run-id", length=999, cursor=None, include_fixit_commands=True + ) assert result == CommandSlice(commands=[], cursor=0, total_length=0) @@ -804,7 +826,42 @@ def test_get_commands_slice_run_not_found(subject: RunStore) -> None: run_id="run-id", protocol_id=None, created_at=datetime.now(timezone.utc) ) with pytest.raises(RunNotFoundError): - subject.get_commands_slice(run_id="not-run-id", cursor=1, length=3) + subject.get_commands_slice( + run_id="not-run-id", cursor=1, length=3, include_fixit_commands=True + ) + + +def test_get_commands_slice_no_fixit_commands( + subject: RunStore, + protocol_commands: List[pe_commands.Command], + state_summary: StateSummary, +) -> None: + """Should raise an error RunNotFoundError.""" + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + subject.update_run_state( + run_id="run-id", + summary=state_summary, + commands=protocol_commands, + run_time_parameters=[], + ) + result = subject.get_commands_slice( + run_id="run-id", + cursor=0, + length=5, + include_fixit_commands=False, + ) + + assert result.cursor == 0 + assert result.total_length == 3 + assert [result_command.id for result_command in result.commands] == [ + "pause-1", + "pause-2", + "pause-3", + ] def test_get_all_commands_as_preserialized_list( @@ -824,12 +881,43 @@ def test_get_all_commands_as_preserialized_list( commands=protocol_commands, run_time_parameters=[], ) - result = subject.get_all_commands_as_preserialized_list(run_id="run-id") + result = subject.get_all_commands_as_preserialized_list( + run_id="run-id", include_fixit_commands=True + ) + assert result == [ + '{"id": "pause-1", "createdAt": "2021-01-01T00:00:00", "commandType": "waitForResume",' + ' "key": "command-key", "status": "succeeded", "params": {"message": "hello world"}, "result": {}, "intent": "protocol"}', + '{"id": "pause-2", "createdAt": "2022-02-02T00:00:00", "commandType": "waitForResume",' + ' "key": "command-key", "status": "succeeded", "params": {"message": "hey world"}, "result": {}, "intent": "protocol"}', + '{"id": "pause-3", "createdAt": "2023-03-03T00:00:00", "commandType": "waitForResume", "key": "command-key", "status": "succeeded", "params": {"message": "sup world"}, "result": {}}', + '{"id": "fixit-pause-1", "createdAt": "2021-01-01T00:00:00", "commandType": "waitForResume", "key": "command-key", "status": "succeeded", "params": {"message": "hello world"}, "result": {}, "intent": "fixit"}', + ] + + +def test_get_all_commands_as_preserialized_list_no_fixit( + subject: RunStore, + protocol_commands: List[pe_commands.Command], + state_summary: StateSummary, +) -> None: + """It should get all commands stored in DB without fixit commands as a pre-serialized list.""" + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + subject.update_run_state( + run_id="run-id", + summary=state_summary, + commands=protocol_commands, + run_time_parameters=[], + ) + result = subject.get_all_commands_as_preserialized_list( + run_id="run-id", include_fixit_commands=False + ) assert result == [ '{"id": "pause-1", "createdAt": "2021-01-01T00:00:00", "commandType": "waitForResume",' - ' "key": "command-key", "status": "succeeded", "params": {"message": "hello world"}, "result": {}}', + ' "key": "command-key", "status": "succeeded", "params": {"message": "hello world"}, "result": {}, "intent": "protocol"}', '{"id": "pause-2", "createdAt": "2022-02-02T00:00:00", "commandType": "waitForResume",' - ' "key": "command-key", "status": "succeeded", "params": {"message": "hey world"}, "result": {}}', - '{"id": "pause-3", "createdAt": "2023-03-03T00:00:00", "commandType": "waitForResume",' - ' "key": "command-key", "status": "succeeded", "params": {"message": "sup world"}, "result": {}}', + ' "key": "command-key", "status": "succeeded", "params": {"message": "hey world"}, "result": {}, "intent": "protocol"}', + '{"id": "pause-3", "createdAt": "2023-03-03T00:00:00", "commandType": "waitForResume", "key": "command-key", "status": "succeeded", "params": {"message": "sup world"}, "result": {}}', ] diff --git a/robot-server/tests/service/legacy/routers/test_networking.py b/robot-server/tests/service/legacy/routers/test_networking.py index a6185c66d7e..22ea2359a92 100755 --- a/robot-server/tests/service/legacy/routers/test_networking.py +++ b/robot-server/tests/service/legacy/routers/test_networking.py @@ -53,6 +53,35 @@ async def mock_is_connected(): assert resp.status_code == 500 +def test_networking_status_tolerates_bad_iface(api_client, monkeypatch): + connection_status = { + "eth0": { + "ipAddress": "169.254.229.173/16", + "macAddress": "B8:27:EB:39:C0:9A", + "gatewayAddress": None, + "state": "connecting (configuring)", + "type": "ethernet", + } + } + + async def mock_is_connected(): + return "full" + + async def mock_get_connection_status(iface): + if iface == nmcli.NETWORK_IFACES.WIFI: + raise ValueError("Oh no!") + else: + return connection_status["eth0"] + + monkeypatch.setattr(nmcli, "is_connected", mock_is_connected) + monkeypatch.setattr(nmcli, "iface_info", mock_get_connection_status) + expected = {"status": "full", "interfaces": connection_status} + resp = api_client.get("/networking/status") + body_json = resp.json() + assert resp.status_code == 200 + assert body_json == expected + + def test_wifi_list(api_client, monkeypatch): expected_res = [ { diff --git a/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py b/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py index b8e41f28d6a..d82a399c022 100644 --- a/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py +++ b/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py @@ -1,32 +1,38 @@ """Tests for the maintenance runs publisher.""" import pytest -from decoy import Decoy +from unittest.mock import AsyncMock, Mock from robot_server.service.notifications import MaintenanceRunsPublisher, topics from robot_server.service.notifications.notification_client import NotificationClient +from robot_server.service.notifications.publisher_notifier import PublisherNotifier @pytest.fixture -def notification_client(decoy: Decoy) -> NotificationClient: +def notification_client() -> Mock: """Mocked notification client.""" - return decoy.mock(cls=NotificationClient) + return Mock(spec_set=NotificationClient) + + +@pytest.fixture +def publisher_notifier() -> Mock: + """Mocked publisher notifier.""" + return Mock(spec_set=PublisherNotifier) @pytest.fixture def maintenance_runs_publisher( - notification_client: NotificationClient, + notification_client: Mock, publisher_notifier: Mock ) -> MaintenanceRunsPublisher: """Instantiate MaintenanceRunsPublisher.""" - return MaintenanceRunsPublisher(notification_client) + return MaintenanceRunsPublisher(notification_client, publisher_notifier) -def test_publish_current_maintenance_run( - notification_client: NotificationClient, - maintenance_runs_publisher: MaintenanceRunsPublisher, - decoy: Decoy, +@pytest.mark.asyncio +async def test_publish_current_maintenance_run( + notification_client: AsyncMock, maintenance_runs_publisher: MaintenanceRunsPublisher ) -> None: """It should publish a notify flag for maintenance runs.""" maintenance_runs_publisher.publish_current_maintenance_run() - decoy.verify( - notification_client.publish_advise_refetch(topics.MAINTENANCE_RUNS_CURRENT_RUN) + notification_client.publish_advise_refetch.assert_called_once_with( + topic=topics.MAINTENANCE_RUNS_CURRENT_RUN ) diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index 1a310dd92ef..263330eb6de 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -73,7 +73,8 @@ "calibration/moveToMaintenancePosition": "#/definitions/MoveToMaintenancePositionCreate", "unsafe/blowOutInPlace": "#/definitions/UnsafeBlowOutInPlaceCreate", "unsafe/dropTipInPlace": "#/definitions/UnsafeDropTipInPlaceCreate", - "unsafe/updatePositionEstimators": "#/definitions/UpdatePositionEstimatorsCreate" + "unsafe/updatePositionEstimators": "#/definitions/UpdatePositionEstimatorsCreate", + "unsafe/engageAxes": "#/definitions/UnsafeEngageAxesCreate" } }, "oneOf": [ @@ -283,13 +284,16 @@ }, { "$ref": "#/definitions/UpdatePositionEstimatorsCreate" + }, + { + "$ref": "#/definitions/UnsafeEngageAxesCreate" } ], "definitions": { "WellOrigin": { "title": "WellOrigin", "description": "Origin of WellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well", - "enum": ["top", "bottom", "center"], + "enum": ["top", "bottom", "center", "meniscus"], "type": "string" }, "WellOffset": { @@ -4468,6 +4472,51 @@ } }, "required": ["params"] + }, + "UnsafeEngageAxesParams": { + "title": "UnsafeEngageAxesParams", + "description": "Payload required for an UnsafeEngageAxes command.", + "type": "object", + "properties": { + "axes": { + "description": "The axes for which to enable.", + "type": "array", + "items": { + "$ref": "#/definitions/MotorAxis" + } + } + }, + "required": ["axes"] + }, + "UnsafeEngageAxesCreate": { + "title": "UnsafeEngageAxesCreate", + "description": "UnsafeEngageAxes command request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "unsafe/engageAxes", + "enum": ["unsafe/engageAxes"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/UnsafeEngageAxesParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] } }, "$id": "opentronsCommandSchemaV9", diff --git a/shared-data/command/types/incidental.ts b/shared-data/command/types/incidental.ts index 9c67ea05106..bb754dcbee5 100644 --- a/shared-data/command/types/incidental.ts +++ b/shared-data/command/types/incidental.ts @@ -1,9 +1,13 @@ import type { CommonCommandRunTimeInfo, CommonCommandCreateInfo } from '.' import type { StatusBarAnimation } from '../../js/types' -export type IncidentalCreateCommand = SetStatusBarCreateCommand +export type IncidentalCreateCommand = + | SetStatusBarCreateCommand + | SetRailLightsCreateCommand -export type IncidentalRunTimeCommand = SetStatusBarRunTimeCommand +export type IncidentalRunTimeCommand = + | SetStatusBarRunTimeCommand + | SetRailLightsRunTimeCommand export interface SetStatusBarCreateCommand extends CommonCommandCreateInfo { commandType: 'setStatusBar' @@ -19,3 +23,18 @@ export interface SetStatusBarRunTimeCommand interface SetStatusBarParams { animation: StatusBarAnimation } + +export interface SetRailLightsCreateCommand extends CommonCommandCreateInfo { + commandType: 'setRailLights' + params: SetRailLightsParams +} + +export interface SetRailLightsRunTimeCommand + extends CommonCommandRunTimeInfo, + SetRailLightsCreateCommand { + result?: any +} + +interface SetRailLightsParams { + on: boolean +} diff --git a/shared-data/command/types/setup.ts b/shared-data/command/types/setup.ts index b3f54cdaad8..85613421b45 100644 --- a/shared-data/command/types/setup.ts +++ b/shared-data/command/types/setup.ts @@ -29,6 +29,15 @@ export interface LoadLabwareRunTimeCommand LoadLabwareCreateCommand { result?: LoadLabwareResult } +export interface ReloadLabwareCreateCommand extends CommonCommandCreateInfo { + commandType: 'reloadLabware' + params: { labwareId: string } +} +export interface ReloadLabwareRunTimeCommand + extends CommonCommandRunTimeInfo, + ReloadLabwareCreateCommand { + result?: ReloadLabwareResult +} export interface MoveLabwareCreateCommand extends CommonCommandCreateInfo { commandType: 'moveLabware' params: MoveLabwareParams @@ -76,6 +85,7 @@ export type SetupRunTimeCommand = | ConfigureNozzleLayoutRunTimeCommand | LoadPipetteRunTimeCommand | LoadLabwareRunTimeCommand + | ReloadLabwareRunTimeCommand | LoadModuleRunTimeCommand | LoadLiquidRunTimeCommand | MoveLabwareRunTimeCommand @@ -84,6 +94,7 @@ export type SetupCreateCommand = | ConfigureNozzleLayoutCreateCommand | LoadPipetteCreateCommand | LoadLabwareCreateCommand + | ReloadLabwareCreateCommand | LoadModuleCreateCommand | LoadLiquidCreateCommand | MoveLabwareCreateCommand @@ -123,8 +134,14 @@ interface LoadLabwareParams { interface LoadLabwareResult { labwareId: string definition: LabwareDefinition2 + // todo(mm, 2024-08-19): This does not match the server-returned offsetId field. + // Confirm nothing client-side is trying to use this, then replace it with offsetId. offset: LabwareOffset } +interface ReloadLabwareResult { + labwareId: string + offsetId?: string | null +} export type LabwareMovementStrategy = | 'usingGripper' diff --git a/shared-data/command/types/unsafe.ts b/shared-data/command/types/unsafe.ts index 8ff4d7e74aa..fd460f573b2 100644 --- a/shared-data/command/types/unsafe.ts +++ b/shared-data/command/types/unsafe.ts @@ -5,11 +5,13 @@ export type UnsafeRunTimeCommand = | UnsafeBlowoutInPlaceRunTimeCommand | UnsafeDropTipInPlaceRunTimeCommand | UnsafeUpdatePositionEstimatorsRunTimeCommand + | UnsafeEngageAxesRunTimeCommand export type UnsafeCreateCommand = | UnsafeBlowoutInPlaceCreateCommand | UnsafeDropTipInPlaceCreateCommand | UnsafeUpdatePositionEstimatorsCreateCommand + | UnsafeEngageAxesCreateCommand export interface UnsafeBlowoutInPlaceParams { pipetteId: string @@ -56,3 +58,17 @@ export interface UnsafeUpdatePositionEstimatorsRunTimeCommand UnsafeUpdatePositionEstimatorsCreateCommand { result?: any } + +export interface UnsafeEngageAxesParams { + axes: MotorAxes +} + +export interface UnsafeEngageAxesCreateCommand extends CommonCommandCreateInfo { + commandType: 'unsafe/engageAxes' + params: UnsafeUpdatePositionEstimatorsParams +} +export interface UnsafeEngageAxesRunTimeCommand + extends CommonCommandRunTimeInfo, + UnsafeEngageAxesCreateCommand { + result?: any +} diff --git a/shared-data/js/__tests__/labwareDefSchemaV3.test.ts b/shared-data/js/__tests__/labwareDefSchemaV3.test.ts new file mode 100644 index 00000000000..8416e8b60c5 --- /dev/null +++ b/shared-data/js/__tests__/labwareDefSchemaV3.test.ts @@ -0,0 +1,70 @@ +import path from 'path' +import glob from 'glob' +import { describe, expect, it, beforeAll, test } from 'vitest' + +import type { LabwareDefinition3 } from '../types' +import Ajv from 'ajv' +import schema from '../../labware/schemas/3.json' + +const fixturesDir = path.join(__dirname, '../../labware/fixtures/3') +const globPattern = '**/*.json' + +const ajv = new Ajv({ allErrors: true, jsonPointers: true }) +const validate = ajv.compile(schema) + +const checkGeometryDefinitions = ( + labwareDef: LabwareDefinition3, + filename: string +): void => { + test(`all geometryDefinitionIds specified in {filename} should have an accompanying valid entry in innerLabwareGeometry`, () => { + for (const wellName in labwareDef.wells) { + const wellGeometryId = labwareDef.wells[wellName].geometryDefinitionId + + if (wellGeometryId === undefined) { + return + } + if ( + labwareDef.innerLabwareGeometry === null || + labwareDef.innerLabwareGeometry === undefined + ) { + return + } + + expect(wellGeometryId in labwareDef.innerLabwareGeometry).toBe(true) + + const wellDepth = labwareDef.wells[wellName].depth + const wellShape = labwareDef.wells[wellName].shape + const topFrustumHeight = + labwareDef.innerLabwareGeometry[wellGeometryId].frusta[0].topHeight + const topFrustumShape = + labwareDef.innerLabwareGeometry[wellGeometryId].frusta[0].shape + + expect(wellDepth).toEqual(topFrustumHeight) + expect(wellShape).toEqual(topFrustumShape) + } + }) +} + +describe(`test additions to labware schema in v3`, () => { + const labwarePaths = glob.sync(globPattern, { cwd: fixturesDir }) + + beforeAll(() => { + // Make sure definitions path didn't break, which would give you false positives + expect(labwarePaths.length).toBeGreaterThan(0) + }) + + labwarePaths.forEach(labwarePath => { + const filename = path.parse(labwarePath).base + const fullLabwarePath = path.join(fixturesDir, labwarePath) + const labwareDef = require(fullLabwarePath) as LabwareDefinition3 + + checkGeometryDefinitions(labwareDef, labwarePath) + + it(`${filename} validates against schema`, () => { + const valid = validate(labwareDef) + const validationErrors = validate.errors + expect(validationErrors).toBe(null) + expect(valid).toBe(true) + }) + }) +}) diff --git a/shared-data/js/__tests__/pipettes.test.ts b/shared-data/js/__tests__/pipettes.test.ts index 123def5a7d3..a30ad6dfe18 100644 --- a/shared-data/js/__tests__/pipettes.test.ts +++ b/shared-data/js/__tests__/pipettes.test.ts @@ -151,15 +151,15 @@ describe('pipette data accessors', () => { }, lldSettings: { t50: { - minHeight: 0.5, + minHeight: 1.0, minVolume: 0, }, t200: { - minHeight: 0.5, + minHeight: 1.0, minVolume: 0, }, t1000: { - minHeight: 0.5, + minHeight: 1.5, minVolume: 0, }, }, diff --git a/shared-data/js/getLabware.ts b/shared-data/js/getLabware.ts index 792263fb938..be1daebb7ea 100644 --- a/shared-data/js/getLabware.ts +++ b/shared-data/js/getLabware.ts @@ -46,14 +46,17 @@ export const LABWAREV2_DO_NOT_LIST = [ 'opentrons_flex_lid_absorbance_plate_reader_module', ] // NOTE(sa, 2020-7-14): in PD we do not want to list calibration blocks -// but we still might want the rest of the labware in LABWAREV2_DO_NOT_LIST -// because of legacy protocols that might use them +// or the adapter/labware combos since we migrated to splitting them up export const PD_DO_NOT_LIST = [ 'opentrons_calibrationblock_short_side_left', 'opentrons_calibrationblock_short_side_right', 'opentrons_96_aluminumblock_biorad_wellplate_200ul', 'opentrons_96_aluminumblock_nest_wellplate_100ul', - 'opentrons_flex_lid_absorbance_plate_reader_module', + 'opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat', + 'opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt', + 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', + 'opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep', + 'opentrons_96_pcr_adapter_armadillo_wellplate_200ul', ] export function getIsLabwareV1Tiprack(def: LabwareDefinition1): boolean { diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 1c03da919ef..dbd8c7f59c7 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -53,6 +53,7 @@ export interface WellDefinition { y: number z: number 'total-liquid-volume': number + geometryDefinitionId?: string | null } // typedef for labware definitions under v1 labware schema @@ -131,19 +132,19 @@ export interface LabwareBrand { links?: string[] } -export interface CircularWellShapeProperties { +export interface CircularWellShape { shape: 'circular' diameter: number } -export interface RectangularWellShapeProperties { +export interface RectangularWellShape { shape: 'rectangular' xDimension: number yDimension: number } export type LabwareWellShapeProperties = - | CircularWellShapeProperties - | RectangularWellShapeProperties + | CircularWellShape + | RectangularWellShape // well without x,y,z export type LabwareWellProperties = LabwareWellShapeProperties & { @@ -155,6 +156,31 @@ export type LabwareWell = LabwareWellProperties & { x: number y: number z: number + geometryDefinitionId?: string +} + +export interface SphericalSegment { + shape: 'spherical' + radiusOfCurvature: number + depth: number +} + +export interface CircularBoundedSection { + shape: 'circular' + diameter: number + topHeight: number +} + +export interface RectangularBoundedSection { + shape: 'rectangular' + xDimension: number + yDimension: number + topHeight: number +} + +export interface InnerWellGeometry { + frusta: CircularBoundedSection[] | RectangularBoundedSection[] + bottomShape?: SphericalSegment | null } // TODO(mc, 2019-03-21): exact object is tough to use with the initial value in @@ -193,6 +219,24 @@ export interface LabwareDefinition2 { stackingOffsetWithModule?: Record } +export interface LabwareDefinition3 { + version: number + schemaVersion: 3 + namespace: string + metadata: LabwareMetadata + dimensions: LabwareDimensions + cornerOffsetFromSlot: LabwareOffset + parameters: LabwareParameters + brand: LabwareBrand + ordering: string[][] + wells: LabwareWellMap + groups: LabwareWellGroup[] + allowedRoles?: LabwareRoles[] + stackingOffsetWithLabware?: Record + stackingOffsetWithModule?: Record + innerLabwareGeometry?: Record | null +} + export interface LabwareDefByDefURI { [defUri: string]: LabwareDefinition2 } diff --git a/shared-data/labware/fixtures/3/fixture_2_plate.json b/shared-data/labware/fixtures/3/fixture_2_plate.json new file mode 100644 index 00000000000..a2e1bb5a3ea --- /dev/null +++ b/shared-data/labware/fixtures/3/fixture_2_plate.json @@ -0,0 +1,100 @@ +{ + "ordering": [["A1"], ["A2"]], + "schemaVersion": 3, + "version": 3, + "namespace": "fixture", + "metadata": { + "displayName": "12 Channel Trough", + "displayVolumeUnits": "mL", + "displayCategory": "reservoir" + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.8, + "zDimension": 44.45 + }, + "parameters": { + "format": "trough", + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "fixture_12_trough", + "quirks": ["centerMultichannelOnWells", "touchTipDisabled"] + }, + "wells": { + "A1": { + "shape": "circular", + "depth": 42.16, + "diameter": 35.0, + "totalLiquidVolume": 22000, + "x": 13.94, + "y": 42.9, + "z": 2.29, + "geometryDefinitionId": "iuweofiuwhfn" + }, + "A2": { + "shape": "rectangular", + "depth": 42.16, + "xDimension": 8.33, + "yDimension": 71.88, + "totalLiquidVolume": 22000, + "x": 23.03, + "y": 42.9, + "z": 2.29, + "geometryDefinitionId": "daiwudhadfhiew" + } + }, + "brand": { + "brand": "USA Scientific", + "brandId": ["1061-8150"] + }, + "groups": [ + { + "wells": ["A1", "A2"], + "metadata": { + "wellBottomShape": "v" + } + } + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "innerLabwareGeometry": { + "daiwudhadfhiew": { + "frusta": [ + { + "shape": "rectangular", + "xDimension": 127.76, + "yDimension": 85.8, + "topHeight": 42.16 + }, + { + "shape": "rectangular", + "xDimension": 70.0, + "yDimension": 50.0, + "topHeight": 20.0 + } + ] + }, + "iuweofiuwhfn": { + "frusta": [ + { + "shape": "circular", + "diameter": 35.0, + "topHeight": 42.16 + }, + { + "shape": "circular", + "diameter": 35.0, + "topHeight": 20.0 + } + ], + "bottomShape": { + "shape": "spherical", + "radiusOfCurvature": 20.0, + "depth": 6.0 + } + } + } +} diff --git a/shared-data/labware/fixtures/3/fixture_corning_24_plate.json b/shared-data/labware/fixtures/3/fixture_corning_24_plate.json new file mode 100644 index 00000000000..d53a6f017ca --- /dev/null +++ b/shared-data/labware/fixtures/3/fixture_corning_24_plate.json @@ -0,0 +1,340 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1"], + ["A2", "B2", "C2", "D2"], + ["A3", "B3", "C3", "D3"], + ["A4", "B4", "C4", "D4"], + ["A5", "B5", "C5", "D5"], + ["A6", "B6", "C6", "D6"] + ], + "brand": { + "brand": "Corning", + "brandId": ["3337", "3524", "3526", "3527", "3473", "3738", "3987"], + "links": [ + "https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3738" + ] + }, + "schemaVersion": 3, + "version": 3, + "namespace": "opentrons", + "metadata": { + "displayName": "Corning 24 Well Plate 3.4 mL Flat", + "displayVolumeUnits": "mL", + "displayCategory": "wellPlate", + "tags": [] + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.47, + "zDimension": 20.27 + }, + "wells": { + "D1": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 17.48, + "y": 13.77, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "C1": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 17.48, + "y": 33.07, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "B1": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 17.48, + "y": 52.37, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "A1": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 17.48, + "y": 71.67, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "D2": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 36.78, + "y": 13.77, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "C2": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 36.78, + "y": 33.07, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "B2": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 36.78, + "y": 52.37, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "A2": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 36.78, + "y": 71.67, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "D3": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 56.08, + "y": 13.77, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "C3": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 56.08, + "y": 33.07, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "B3": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 56.08, + "y": 52.37, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "A3": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 56.08, + "y": 71.67, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "D4": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 75.38, + "y": 13.77, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "C4": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 75.38, + "y": 33.07, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "B4": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 75.38, + "y": 52.37, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "A4": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 75.38, + "y": 71.67, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "D5": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 94.68, + "y": 13.77, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "C5": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 94.68, + "y": 33.07, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "B5": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 94.68, + "y": 52.37, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "A5": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 94.68, + "y": 71.67, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "D6": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 113.98, + "y": 13.77, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "C6": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 113.98, + "y": 33.07, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "B6": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 113.98, + "y": 52.37, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + }, + "A6": { + "shape": "circular", + "depth": 17.4, + "diameter": 16.26, + "totalLiquidVolume": 3400, + "x": 113.98, + "y": 71.67, + "z": 2.87, + "geometryDefinitionId": "venirhgerug" + } + }, + "parameters": { + "format": "irregular", + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "corning_24_wellplate_3.4ml_flat" + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "A2", + "B2", + "C2", + "D2", + "A3", + "B3", + "C3", + "D3", + "A4", + "B4", + "C4", + "D4", + "A5", + "B5", + "C5", + "D5", + "A6", + "B6", + "C6", + "D6" + ], + "metadata": { + "wellBottomShape": "flat" + } + } + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "stackingOffsetWithLabware": { + "opentrons_aluminum_flat_bottom_plate": { + "x": 0, + "y": 0, + "z": 4.15 + } + }, + "innerLabwareGeometry": { + "venirhgerug": { + "frusta": [ + { + "shape": "circular", + "diameter": 16.26, + "topHeight": 17.4 + }, + { + "shape": "circular", + "diameter": 16.26, + "topHeight": 0.0 + } + ] + } + } +} diff --git a/shared-data/labware/repository/fixtures/1/exampleLabwareRepository.json b/shared-data/labware/repository/fixtures/1/exampleLabwareRepository.json new file mode 100644 index 00000000000..2a74a39b000 --- /dev/null +++ b/shared-data/labware/repository/fixtures/1/exampleLabwareRepository.json @@ -0,0 +1,43 @@ +{ + "metadata": { + "name": "Example Labware Repository", + "owner": "Opentrons Example App", + "lastModifiedDate": "2024-06-11T15:00:00Z" + }, + "123e4567-e89b-12d3-a456-426614174000": { + "labwareUri": "http://example.com/labware/123e4567-e89b-12d3-a456-426614174000", + "definitionPath": "definitions/labware1.json", + "additionalContent": [ + { + "contentMimeType": "image/png", + "semanticType": "topViewImage", + "contentPath": "images/labware1_top.png" + }, + { + "contentMimeType": "image/png", + "semanticType": "orthographicViewImage", + "contentPath": "images/labware1_ortho.png" + } + ], + "lastEditTimestamp": "2024-06-10T12:00:00Z", + "valid": true + }, + "223e4567-e89b-12d3-a456-426614174001": { + "labwareUri": "http://example.com/labware/223e4567-e89b-12d3-a456-426614174001", + "definitionPath": "definitions/labware2.json", + "additionalContent": [ + { + "contentMimeType": "image/jpeg", + "semanticType": "longSideViewImage", + "contentPath": "images/labware2_long.jpg" + }, + { + "contentMimeType": "image/jpeg", + "semanticType": "shortSideViewImage", + "contentPath": "images/labware2_short.jpg" + } + ], + "lastEditTimestamp": "2024-06-10T13:00:00Z", + "valid": false + } +} diff --git a/shared-data/labware/repository/schemas/1.json b/shared-data/labware/repository/schemas/1.json new file mode 100644 index 00000000000..c7878916482 --- /dev/null +++ b/shared-data/labware/repository/schemas/1.json @@ -0,0 +1,81 @@ +{ + "$id": "opentronsLabwareRepositorySchemaV1", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "format": "This schema defines the labware repository interface. Labware entries must be keyed by labware def uri, with values representing each labware's data", + "properties": { + "metadata": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "name of the labware repository" + }, + "owner": { + "type": "string", + "description": "owner of the labware repository" + }, + "lastModifiedDate": { + "type": "string", + "format": "date-time" + } + }, + "required": ["name", "owner", "lastModifiedDate"] + } + }, + "patternProperties": { + "^[0-9a-fA-F-]{36}$": { + "type": "object", + "properties": { + "labwareDefUri": { + "type": "string", + "format": "labware definition uri" + }, + "definitionPath": { + "type": "string", + "description": "a path to the labware definition relative to the index file’s containing directory" + }, + "additionalContent": { + "type": "array", + "description": "additional metadata for file contents for this labware", + "items": { + "type": "object", + "properties": { + "contentMimeType": { + "type": "string" + }, + "semanticType": { + "type": "string", + "enum": [ + "orthographicViewImage", + "topViewImage", + "longSideViewImage", + "shortSideViewImage" + ] + }, + "contentPath": { + "type": "string" + } + }, + "required": ["contentMimeType", "semanticType", "contentPath"] + } + }, + "lastEditTimestamp": { + "type": "string", + "format": "date-time" + }, + "valid": { + "type": "boolean" + } + }, + "required": [ + "labwareDefUri", + "definitionPath", + "additionalContent", + "lastEditTimestamp", + "valid" + ] + } + }, + "additionalProperties": false +} diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json new file mode 100644 index 00000000000..e03b1c8f064 --- /dev/null +++ b/shared-data/labware/schemas/3.json @@ -0,0 +1,462 @@ +{ + "$id": "opentronsLabwareSchemaV3", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "positiveNumber": { + "type": "number", + "minimum": 0 + }, + "brandData": { + "type": "object", + "additionalProperties": false, + "required": ["brand"], + "properties": { + "brand": { + "type": "string", + "description": "Brand/manufacturer name" + }, + "brandId": { + "type": "array", + "description": "An array of manufacture numbers pertaining to a given labware", + "items": { + "type": "string" + } + }, + "links": { + "type": "array", + "description": "URLs for manufacturer page(s)", + "items": { + "type": "string" + } + } + } + }, + "displayCategory": { + "type": "string", + "enum": [ + "tipRack", + "tubeRack", + "reservoir", + "trash", + "wellPlate", + "aluminumBlock", + "adapter", + "other" + ] + }, + "safeString": { + "description": "a string safe to use for loadName / namespace. Lowercase-only.", + "type": "string", + "pattern": "^[a-z0-9._]+$" + }, + "coordinates": { + "type": "object", + "additionalProperties": false, + "required": ["x", "y", "z"], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "SphericalSegment": { + "type": "object", + "additionalProperties": false, + "required": ["shape", "radiusOfCurvature", "depth"], + "properties": { + "shape": { + "type": "string", + "enum": ["spherical"] + }, + "radiusOfCurvature": { + "type": "number" + }, + "depth": { + "type": "number" + } + } + }, + "CircularBoundedSection": { + "type": "object", + "required": ["shape", "diameter", "topHeight"], + "properties": { + "shape": { + "type": "string", + "enum": ["circular"] + }, + "diameter": { + "type": "number" + }, + "topHeight": { + "type": "number", + "description": "The height at the top of a bounded subsection of a well, relative to the bottom" + } + } + }, + "RectangularBoundedSection": { + "type": "object", + "required": ["shape", "xDimension", "yDimension", "topHeight"], + "properties": { + "shape": { + "type": "string", + "enum": ["rectangular"] + }, + "xDimension": { + "type": "number" + }, + "yDimension": { + "type": "number" + }, + "topHeight": { + "type": "number", + "description": "The height at the top of a bounded subsection of a well, relative to the bottom" + } + } + }, + "InnerWellGeometry": { + "type": "object", + "required": ["frusta"], + "properties": { + "frusta": { + "description": "A list of all of the sections of the well that have a contiguous shape", + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/CircularBoundedSection" + }, + { + "$ref": "#/definitions/RectangularBoundedSection" + } + ] + } + }, + "bottomShape": { + "type": "object", + "description": "The shape at the bottom of the well: either a spherical segment or a cross-section", + "$ref": "#/definitions/SphericalSegment" + } + } + } + }, + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "version", + "namespace", + "metadata", + "brand", + "parameters", + "cornerOffsetFromSlot", + "ordering", + "dimensions", + "wells", + "groups" + ], + "properties": { + "schemaVersion": { + "description": "Which schema version a labware is using", + "type": "number", + "enum": [3] + }, + "version": { + "description": "Version of the labware definition itself (eg myPlate v1/v2/v3). An incrementing integer", + "type": "integer", + "minimum": 1 + }, + "namespace": { + "$ref": "#/definitions/safeString" + }, + "metadata": { + "type": "object", + "description": "Properties used for search and display", + "additionalProperties": false, + "required": ["displayName", "displayCategory", "displayVolumeUnits"], + "properties": { + "displayName": { + "description": "Easy to remember name of labware", + "type": "string" + }, + "displayCategory": { + "$ref": "#/definitions/displayCategory", + "description": "Label(s) used in UI to categorize labware" + }, + "displayVolumeUnits": { + "description": "Volume units for display", + "type": "string", + "enum": ["µL", "mL", "L"] + }, + "tags": { + "type": "array", + "description": "List of descriptions for a given labware", + "items": { + "type": "string" + } + } + } + }, + "brand": { + "$ref": "#/definitions/brandData", + "description": "Real-world labware that the definition is modeled from and/or compatible with" + }, + "parameters": { + "type": "object", + "description": "Internal describers used to determine pipette movement to labware", + "additionalProperties": false, + "required": [ + "format", + "isTiprack", + "loadName", + "isMagneticModuleCompatible" + ], + "properties": { + "format": { + "description": "Property to determine compatibility with multichannel pipette", + "type": "string", + "enum": ["96Standard", "384Standard", "trough", "irregular", "trash"] + }, + "quirks": { + "description": "Property to classify a specific behavior this labware should have", + "type": "array", + "items": { + "type": "string" + } + }, + "isTiprack": { + "description": "Flag marking whether this labware is a tip rack or not", + "type": "boolean" + }, + "tipLength": { + "description": "Required if this labware is a tip rack. Specifies the total length of one of this rack's tips, from top to bottom, as specified by technical drawings or as measured with calipers.", + "$ref": "#/definitions/positiveNumber" + }, + "tipOverlap": { + "description": "Required if this labware is a tip rack. Specifies how far one of this rack's tips is expected to overlap with the nozzle of a pipette. In other words: tipLength, minus the distance between the bottom of the pipette and the bottom of the tip. A robot's positional calibration may fine-tune this estimate.", + "$ref": "#/definitions/positiveNumber" + }, + "loadName": { + "description": "Name used to reference a labware definition", + "$ref": "#/definitions/safeString" + }, + "isMagneticModuleCompatible": { + "description": "Flag marking whether a labware is compatible by default with the Magnetic Module", + "type": "boolean" + }, + "magneticModuleEngageHeight": { + "description": "Distance to move magnetic module magnets to engage", + "$ref": "#/definitions/positiveNumber" + } + } + }, + "ordering": { + "type": "array", + "description": "Generated array that keeps track of how wells should be ordered in a labware", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "cornerOffsetFromSlot": { + "type": "object", + "additionalProperties": false, + "description": "Distance from left-front-bottom corner of slot to left-front-bottom corner of labware bounding box. Used for labware that spans multiple slots. For labware that does not span multiple slots, x/y/z should all be zero.", + "required": ["x", "y", "z"], + "properties": { + "x": { "type": "number" }, + "y": { "type": "number" }, + "z": { "type": "number" } + } + }, + "dimensions": { + "type": "object", + "additionalProperties": false, + "description": "Outer dimensions of a labware", + "required": ["xDimension", "yDimension", "zDimension"], + "properties": { + "yDimension": { + "$ref": "#/definitions/positiveNumber" + }, + "zDimension": { + "$ref": "#/definitions/positiveNumber" + }, + "xDimension": { + "$ref": "#/definitions/positiveNumber" + } + } + }, + "wells": { + "type": "object", + "description": "Unordered object of well objects with position and dimensional information", + "additionalProperties": false, + "patternProperties": { + "[A-Z]+[0-9]+": { + "type": "object", + "additionalProperties": false, + "required": ["depth", "shape", "totalLiquidVolume", "x", "y", "z"], + "oneOf": [ + { "required": ["xDimension", "yDimension"] }, + { "required": ["diameter"] } + ], + "not": { + "anyOf": [ + { "required": ["diameter", "xDimension"] }, + { "required": ["diameter", "yDimension"] } + ] + }, + "properties": { + "depth": { + "description": "The distance between the top and bottom of this well. If the labware is a tip rack, this will be ignored in favor of tipLength, but the values should match.", + "$ref": "#/definitions/positiveNumber" + }, + "x": { + "description": "x location of center-bottom of well in reference to left-front-bottom of labware", + "$ref": "#/definitions/positiveNumber" + }, + "y": { + "description": "y location of center-bottom of well in reference to left-front-bottom of labware", + "$ref": "#/definitions/positiveNumber" + }, + "z": { + "description": "z location of center-bottom of well in reference to left-front-bottom of labware", + "$ref": "#/definitions/positiveNumber" + }, + "totalLiquidVolume": { + "description": "Total well, tube, or tip volume in microliters", + "$ref": "#/definitions/positiveNumber" + }, + "xDimension": { + "description": "x dimension of rectangular wells", + "$ref": "#/definitions/positiveNumber" + }, + "yDimension": { + "description": "y dimension of rectangular wells", + "$ref": "#/definitions/positiveNumber" + }, + "diameter": { + "description": "diameter of circular wells", + "$ref": "#/definitions/positiveNumber" + }, + "shape": { + "description": "If 'rectangular', use xDimension and yDimension; if 'circular' use diameter", + "type": "string", + "enum": ["rectangular", "circular"] + }, + "geometryDefinitionId": { + "description": "string id of the well's corresponding innerWellGeometry", + "type": ["string", "null"] + } + } + } + } + }, + "groups": { + "type": "array", + "description": "Logical well groupings for metadata/display purposes; changes in groups do not affect protocol execution", + "items": { + "type": "object", + "required": ["wells", "metadata"], + "additionalProperties": false, + "properties": { + "wells": { + "type": "array", + "description": "An array of wells that contain the same metadata", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "description": "Metadata specific to a grid of wells in a labware", + "required": [], + "additionalProperties": false, + "properties": { + "displayName": { + "type": "string", + "description": "User-readable name for the well group" + }, + "displayCategory": { + "$ref": "#/definitions/displayCategory", + "description": "Label(s) used in UI to categorize well groups" + }, + "wellBottomShape": { + "type": "string", + "description": "Bottom shape of the well for UI purposes", + "enum": ["flat", "u", "v"] + } + } + }, + "brand": { + "$ref": "#/definitions/brandData", + "description": "Brand data for the well group (e.g. for tubes)" + } + } + } + }, + "allowedRoles": { + "type": "array", + "description": "Allowed behaviors and usage of a labware in a protocol.", + "items": { + "type": "string", + "enum": ["labware", "adapter", "fixture", "maintenance"] + } + }, + "stackingOffsetWithLabware": { + "type": "object", + "description": "Supported labware that can be stacked upon, with overlap height between both labware.", + "additionalProperties": { + "$ref": "#/definitions/coordinates" + } + }, + "stackingOffsetWithModule": { + "type": "object", + "description": "Supported module that can be stacked upon, with overlap height between labware and module.", + "additionalProperties": { + "$ref": "#/definitions/coordinates" + } + }, + "gripperOffsets": { + "type": "object", + "description": "Offsets to be added when calculating the coordinates a gripper should go to when picking up or dropping a labware on this labware.", + "properties": { + "default": { + "type": "object", + "properties": { + "pickUpOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate pick-up coordinates of a labware placed on this labware." + }, + "dropOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate drop coordinates of a labware placed on this labware." + } + }, + "required": ["pickUpOffset", "dropOffset"] + } + } + }, + "gripForce": { + "type": "number", + "description": "Force, in Newtons, with which the gripper should grip the labware." + }, + "gripHeightFromLabwareBottom": { + "type": "number", + "description": "Recommended Z-height, from labware bottom to the center of gripper pads, when gripping the labware." + }, + "innerLabwareGeometry": { + "type": ["object", "null"], + "description": "A dictionary holding all unique inner well geometries in a labware.", + "additionalProperties": { + "$ref": "#/definitions/InnerWellGeometry" + } + } + } +} diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json index 0ac9da8be5a..9747bafc1d7 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json @@ -189,7 +189,7 @@ }, "t200": { "speed": 10.0, - "distance": 12, + "distance": 11, "current": 0.4, "tipOverlaps": { "v0": { @@ -206,7 +206,7 @@ }, "t50": { "speed": 10.0, - "distance": 12, + "distance": 10.85, "current": 0.4, "tipOverlaps": { "v0": { @@ -267,7 +267,7 @@ }, "t200": { "speed": 10.0, - "distance": 12, + "distance": 11, "current": 0.4, "tipOverlaps": { "v0": { @@ -284,7 +284,7 @@ }, "t50": { "speed": 10.0, - "distance": 12, + "distance": 10.85, "current": 0.4, "tipOverlaps": { "v0": { @@ -345,7 +345,7 @@ }, "t200": { "speed": 10.0, - "distance": 12, + "distance": 11, "current": 0.4, "tipOverlaps": { "v0": { @@ -362,7 +362,7 @@ }, "t50": { "speed": 10.0, - "distance": 12, + "distance": 10.85, "current": 0.4, "tipOverlaps": { "v0": { @@ -423,7 +423,7 @@ }, "t200": { "speed": 10.0, - "distance": 12, + "distance": 11, "current": 0.4, "tipOverlaps": { "v0": { @@ -440,7 +440,7 @@ }, "t50": { "speed": 10.0, - "distance": 12, + "distance": 10.85, "current": 0.4, "tipOverlaps": { "v0": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json index 43d4412d0d6..42a96a5d73b 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json @@ -189,7 +189,7 @@ }, "t200": { "speed": 10.0, - "distance": 12, + "distance": 11, "current": 0.4, "tipOverlaps": { "v0": { @@ -206,7 +206,7 @@ }, "t50": { "speed": 10.0, - "distance": 12, + "distance": 10.85, "current": 0.4, "tipOverlaps": { "v0": { @@ -267,7 +267,7 @@ }, "t200": { "speed": 10.0, - "distance": 12, + "distance": 11, "current": 0.4, "tipOverlaps": { "v0": { @@ -284,7 +284,7 @@ }, "t50": { "speed": 10.0, - "distance": 12, + "distance": 10.85, "current": 0.4, "tipOverlaps": { "v0": { @@ -345,7 +345,7 @@ }, "t200": { "speed": 10.0, - "distance": 12, + "distance": 11, "current": 0.4, "tipOverlaps": { "v0": { @@ -362,7 +362,7 @@ }, "t50": { "speed": 10.0, - "distance": 12, + "distance": 10.85, "current": 0.4, "tipOverlaps": { "v0": { @@ -423,7 +423,7 @@ }, "t200": { "speed": 10.0, - "distance": 12, + "distance": 11, "current": 0.4, "tipOverlaps": { "v0": { @@ -440,7 +440,7 @@ }, "t50": { "speed": 10.0, - "distance": 12, + "distance": 10.85, "current": 0.4, "tipOverlaps": { "v0": { diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json index bc76a2669d5..a6294f329cd 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json @@ -40,15 +40,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json index bc76a2669d5..a6294f329cd 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json @@ -40,15 +40,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json index bc76a2669d5..a6294f329cd 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json @@ -40,15 +40,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json index bc76a2669d5..a6294f329cd 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json @@ -40,15 +40,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json index 9bbd489e296..4bd9c9048f9 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json @@ -40,7 +40,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json index 9bbd489e296..4bd9c9048f9 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json @@ -40,7 +40,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json index 9bbd489e296..4bd9c9048f9 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json @@ -40,7 +40,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json index 9bbd489e296..4bd9c9048f9 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json @@ -40,7 +40,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json index b0c41b25661..54ef5a9f4a3 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json @@ -294,15 +294,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 2.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json index b0c41b25661..54ef5a9f4a3 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json @@ -294,15 +294,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 2.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json index b0c41b25661..54ef5a9f4a3 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json @@ -294,15 +294,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 2.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json index b0c41b25661..54ef5a9f4a3 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json @@ -294,15 +294,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 2.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json index b0c41b25661..54ef5a9f4a3 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json @@ -294,15 +294,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 2.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json index b30d15ddb66..8eb3dfb9e97 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json @@ -13,15 +13,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json index b30d15ddb66..8eb3dfb9e97 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json @@ -13,15 +13,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json index b30d15ddb66..8eb3dfb9e97 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json @@ -13,15 +13,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json index b30d15ddb66..8eb3dfb9e97 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json @@ -13,15 +13,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_6.json index b30d15ddb66..8eb3dfb9e97 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_6.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_6.json @@ -13,15 +13,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_7.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_7.json index b30d15ddb66..8eb3dfb9e97 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_7.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_7.json @@ -13,15 +13,15 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t200": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 }, "t1000": { - "minHeight": 0.5, + "minHeight": 1.5, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json index b40f6957277..3b595351689 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json @@ -13,7 +13,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json index b40f6957277..3b595351689 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json @@ -13,7 +13,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json index b40f6957277..3b595351689 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json @@ -13,7 +13,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json index b40f6957277..3b595351689 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json @@ -13,7 +13,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_6.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_6.json index b40f6957277..3b595351689 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_6.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_6.json @@ -13,7 +13,7 @@ }, "lldSettings": { "t50": { - "minHeight": 0.5, + "minHeight": 1.0, "minVolume": 0 } } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json index 0dae86106ce..43e7cb88798 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json @@ -180,6 +180,9 @@ "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_1000ul/1", "opentrons/opentrons_flex_96_tiprack_200ul/1", - "opentrons/opentrons_flex_96_tiprack_50ul/1" + "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1" ] } diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index e033ee144f7..b27b3c9c3d3 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -1,5 +1,5 @@ """Exception hierarchy for error codes.""" -from typing import Dict, Any, Optional, List, Iterator, Union, Sequence +from typing import Dict, Any, Optional, List, Iterator, Union, Sequence, overload from logging import getLogger from traceback import format_exception_only, format_tb import inspect @@ -900,12 +900,37 @@ def __init__( class APIRemoved(GeneralError): """An error indicating that a specific API is no longer available.""" - def __init__( + @overload + def __init__( # noqa: D107 self, + *, api_element: Optional[str] = None, since_version: Optional[str] = None, current_version: Optional[str] = None, + extra_message: Optional[str] = None, + detail: Optional[Dict[str, str]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + pass + + @overload + def __init__( # noqa: D107 + self, message: Optional[str] = None, + *, + detail: Optional[Dict[str, str]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + pass + + def __init__( + self, + message: Optional[str] = None, + *, + api_element: Optional[str] = None, + since_version: Optional[str] = None, + current_version: Optional[str] = None, + extra_message: Optional[str] = None, detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: @@ -914,23 +939,27 @@ def __init__( checked_detail["identifier"] = api_element checked_detail["since_version"] = since_version checked_detail["current_version"] = current_version - checked_message = "" - if api_element and since_version and current_version: - checked_message = f"{api_element} is not available after API version {since_version}. You are currently using API version {current_version}." - elif api_element and since_version: - checked_message = ( - f"{api_element} is not available after API version {since_version}." - ) - elif api_element: - checked_message = ( - f"{api_element} is no longer available in the API version in use." - ) - if message: - checked_message = checked_message + message - checked_message = ( - checked_message - or "This feature is no longer available in the API version in use." - ) + + checked_api_element = api_element if api_element is not None else "This feature" + + if message is not None: + checked_message = message + else: + if since_version is not None and current_version is not None: + checked_message = ( + f"{checked_api_element} is not available after API version {since_version}." + f" You are currently using API version {current_version}." + ) + elif since_version is not None and current_version is None: + checked_message = f"{checked_api_element} is not available after API version {since_version}." + elif since_version is None and current_version is not None: + checked_message = f"{checked_api_element} is not available in API version {current_version}." + else: + checked_message = f"{checked_api_element} is no longer available in the API version in use." + + if extra_message is not None: + checked_message += " " + extra_message + super().__init__( ErrorCodes.API_REMOVED, checked_message, checked_detail, wrapping ) @@ -939,12 +968,37 @@ def __init__( class IncorrectAPIVersion(GeneralError): """An error indicating that a command was issued that is not supported by the API version in use.""" - def __init__( + @overload + def __init__( # noqa: D107 self, + *, api_element: Optional[str] = None, until_version: Optional[str] = None, current_version: Optional[str] = None, + extra_message: Optional[str] = None, + detail: Optional[Dict[str, str]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + pass + + @overload + def __init__( # noqa: D107 + self, message: Optional[str] = None, + *, + detail: Optional[Dict[str, str]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + pass + + def __init__( + self, + message: Optional[str] = None, + *, + api_element: Optional[str] = None, + until_version: Optional[str] = None, + current_version: Optional[str] = None, + extra_message: Optional[str] = None, detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: @@ -953,22 +1007,27 @@ def __init__( checked_detail["identifier"] = api_element checked_detail["until_version"] = until_version checked_detail["current_version"] = current_version - if api_element and until_version and current_version: - checked_message = f"{api_element} is not available until API version {until_version}. You are currently using API version {current_version}." - elif api_element and until_version: - checked_message = ( - f"{api_element} is not available until API version {until_version}." - ) - elif api_element: - checked_message = ( - f"{api_element} is not yet available in the API version in use." - ) - if message: - checked_message = checked_message + " " + message - checked_message = ( - checked_message - or "This feature is not yet available in the API version in use." - ) + + checked_api_element = api_element if api_element is not None else "This feature" + + if message is not None: + checked_message = message + else: + if until_version is not None and current_version is not None: + checked_message = ( + f"{checked_api_element} is not available until API version {until_version}." + f" You are currently using API version {current_version}." + ) + elif until_version is not None and current_version is None: + checked_message = f"{checked_api_element} is not available until API version {until_version}." + elif until_version is None and current_version is not None: + checked_message = f"{checked_api_element} is not available in API version {current_version}." + else: + checked_message = f"{checked_api_element} is not yet available in the API version in use." + + if extra_message is not None: + checked_message += " " + extra_message + super().__init__( ErrorCodes.INCORRECT_API_VERSION, checked_message, checked_detail, wrapping ) diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 198a4854108..bae7a6d9366 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -227,27 +227,6 @@ class Config: ) -class CircularCrossSection(BaseModel): - shape: Literal["circular"] = Field(..., description="Denote shape as circular") - diameter: _NonNegativeNumber = Field( - ..., description="The diameter of a circular cross section of a well" - ) - - -class RectangularCrossSection(BaseModel): - shape: Literal["rectangular"] = Field( - ..., description="Denote shape as rectangular" - ) - xDimension: Optional[_NonNegativeNumber] = Field( - None, - description="x dimension of a subsection of wells", - ) - yDimension: Optional[_NonNegativeNumber] = Field( - None, - description="y dimension of a subsection of wells", - ) - - class SphericalSegment(BaseModel): shape: Literal["spherical"] = Field(..., description="Denote shape as spherical") radius_of_curvature: _NonNegativeNumber = Field( @@ -259,15 +238,29 @@ class SphericalSegment(BaseModel): ) -TopCrossSection = Union[CircularCrossSection, RectangularCrossSection] -BottomShape = Union[CircularCrossSection, RectangularCrossSection, SphericalSegment] +class CircularBoundedSection(BaseModel): + shape: Literal["circular"] = Field(..., description="Denote shape as circular") + diameter: _NonNegativeNumber = Field( + ..., description="The diameter of a circular cross section of a well" + ) + topHeight: _NonNegativeNumber = Field( + ..., + description="The height at the top of a bounded subsection of a well, relative to the bottom" + "of the well", + ) -class BoundedSection(BaseModel): - geometry: TopCrossSection = Field( +class RectangularBoundedSection(BaseModel): + shape: Literal["rectangular"] = Field( + ..., description="Denote shape as rectangular" + ) + xDimension: _NonNegativeNumber = Field( ..., - description="Geometrical information needed to calculate the volume of a subsection of a well", - discriminator="shape", + description="x dimension of a subsection of wells", + ) + yDimension: _NonNegativeNumber = Field( + ..., + description="y dimension of a subsection of wells", ) topHeight: _NonNegativeNumber = Field( ..., @@ -305,14 +298,15 @@ class Group(BaseModel): class InnerWellGeometry(BaseModel): - frusta: List[BoundedSection] = Field( + frusta: Union[ + List[CircularBoundedSection], List[RectangularBoundedSection] + ] = Field( ..., description="A list of all of the sections of the well that have a contiguous shape", ) - bottomShape: BottomShape = Field( - ..., + bottomShape: Optional[SphericalSegment] = Field( + None, description="The shape at the bottom of the well: either a spherical segment or a cross-section", - discriminator="shape", ) diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index f804d8e2521..9ea7a83fb6b 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -3,8 +3,8 @@ types in this file by and large require the use of typing_extensions. this module shouldn't be imported unless typing.TYPE_CHECKING is true. """ -from typing import Dict, List, NewType, Union -from typing_extensions import Literal, TypedDict, NotRequired +from typing import Dict, List, NewType, Union, Optional, Any +from typing_extensions import Literal, TypedDict, NotRequired, TypeGuard LabwareUri = NewType("LabwareUri", str) @@ -120,35 +120,40 @@ class WellGroup(TypedDict, total=False): brand: LabwareBrandData -class CircularCrossSection(TypedDict): - shape: Circular - diameter: float +class SphericalSegment(TypedDict): + shape: Spherical + radiusOfCurvature: float + depth: float -class RectangularCrossSection(TypedDict): +class RectangularBoundedSection(TypedDict): shape: Rectangular xDimension: float yDimension: float + topHeight: float -class SphericalSegment(TypedDict): - shape: Spherical - radius_of_curvature: float - depth: float +class CircularBoundedSection(TypedDict): + shape: Circular + diameter: float + topHeight: float -TopCrossSection = Union[CircularCrossSection, RectangularCrossSection] -BottomShape = Union[CircularCrossSection, RectangularCrossSection, SphericalSegment] +def is_circular_frusta_list( + items: List[Any], +) -> TypeGuard[List[CircularBoundedSection]]: + return all(item.shape == "circular" for item in items) -class BoundedSection(TypedDict): - geometry: TopCrossSection - topHeight: float +def is_rectangular_frusta_list( + items: List[Any], +) -> TypeGuard[List[RectangularBoundedSection]]: + return all(item.shape == "rectangular" for item in items) class InnerWellGeometry(TypedDict): - frusta: List[BoundedSection] - bottomShape: BottomShape + frusta: Union[List[CircularBoundedSection], List[RectangularBoundedSection]] + bottomShape: Optional[SphericalSegment] class LabwareDefinition(TypedDict): diff --git a/shared-data/python/opentrons_shared_data/robot/types.py b/shared-data/python/opentrons_shared_data/robot/types.py index e478957bc29..81d27fd34d7 100644 --- a/shared-data/python/opentrons_shared_data/robot/types.py +++ b/shared-data/python/opentrons_shared_data/robot/types.py @@ -37,6 +37,15 @@ class mountOffset(TypedDict): gripper: NotRequired[List[float]] +class paddingOffset(TypedDict): + """The padding offsets for a given robot type based off how far the pipettes can travel beyond the deck extents.""" + + rear: float + front: float + leftSide: float + rightSide: float + + class RobotDefinition(TypedDict): """A python version of the robot definition type.""" @@ -44,4 +53,5 @@ class RobotDefinition(TypedDict): robotType: RobotType models: List[str] extents: List[float] + paddingOffsets: paddingOffset mountOffsets: mountOffset diff --git a/shared-data/python/tests/labware/__init__.py b/shared-data/python/tests/labware/__init__.py index f6b27fc8dc1..04744150d7a 100644 --- a/shared-data/python/tests/labware/__init__.py +++ b/shared-data/python/tests/labware/__init__.py @@ -11,3 +11,6 @@ def get_ot_defs() -> List[Tuple[str, int]]: # example filename # shared-data/labware/definitions/2/opentrons_96_tiprack_300ul/1.json return [(f.parent.name, int(f.stem)) for f in def_files] + + +# TODO(cm): add python validation once labware definitions are added diff --git a/shared-data/robot/definitions/1/ot2.json b/shared-data/robot/definitions/1/ot2.json index 50c6eb4256a..c1199f86045 100644 --- a/shared-data/robot/definitions/1/ot2.json +++ b/shared-data/robot/definitions/1/ot2.json @@ -3,6 +3,12 @@ "robotType": "OT-2 Standard", "models": ["OT-2 Standard", "OT-2 Refresh"], "extents": [446.75, 347.5, 0.0], + "paddingOffsets": { + "rear": -35.91, + "front": 31.89, + "leftSide": 0, + "rightSide": 0 + }, "mountOffsets": { "left": [-34.0, 0.0, 0.0], "right": [0.0, 0.0, 0.0] diff --git a/shared-data/robot/definitions/1/ot3.json b/shared-data/robot/definitions/1/ot3.json index eb3a943d886..05b0db6928c 100644 --- a/shared-data/robot/definitions/1/ot3.json +++ b/shared-data/robot/definitions/1/ot3.json @@ -3,6 +3,12 @@ "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32 + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/shared-data/robot/schemas/1.json b/shared-data/robot/schemas/1.json index 44e25e6caf5..f0c50eb0ca5 100644 --- a/shared-data/robot/schemas/1.json +++ b/shared-data/robot/schemas/1.json @@ -37,6 +37,29 @@ "description": "The maximum addressable coordinates of the deck without instruments.", "$ref": "#/definitions/xyzArray" }, + "paddingOffsets": { + "description": "The distance from a given edge of a deck extent by which the maximum amount of travel is limited.", + "type": "object", + "required": ["rear", "front", "leftSide", "rightSide"], + "properties": { + "rear": { + "description": "The padding distance from the rear edge of the deck extents which the front nozzles of a pipette must not exceed.", + "type": "number" + }, + "front": { + "description": "The padding distance from the front edge of the deck extents which the rear nozzles of a pipette must not exceed.", + "type": "number" + }, + "leftSide": { + "description": "The padding distance from the left edge of the deck extents which the right-most nozzles of a pipette must not exceed.", + "type": "number" + }, + "rightSide": { + "description": "The padding distance from the right edge of the deck extents which the left-most nozzles of a pipette must not exceed.", + "type": "number" + } + } + }, "mountOffsets": { "description": "The physical mount offsets from the center of the instrument carriage.", "type": "object", diff --git a/yarn.lock b/yarn.lock index 85b473cbd44..b49e439640d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3278,7 +3278,7 @@ redux-thunk "2.3.0" reselect "4.0.0" rxjs "^6.5.1" - semver "5.5.0" + semver "5.7.2" styled-components "5.3.6" typeface-open-sans "0.0.75" uuid "3.2.1" @@ -3308,7 +3308,7 @@ version "0.0.0-dev" dependencies: "@types/lodash" "^4.14.191" - "@types/node-fetch" "^2.5.8" + "@types/node-fetch" "2.6.11" "@types/yargs" "17.0.32" escape-string-regexp "1.0.5" is-ip "3.1.0" @@ -5469,7 +5469,7 @@ resolved "https://registry.yarnpkg.com/@types/netmask/-/netmask-1.0.30.tgz#b68005e3e3c19f517ced4610bb69dce2e0c5babb" integrity sha512-Kl1xAICLv1Y7/WsNXkPKldRMz3QmXUYMIzr3rMXnIBDy9c4/sYG7V6P6u7Ja3w+uNtNQrRudJduqVoYX/DxfZg== -"@types/node-fetch@2.6.11", "@types/node-fetch@^2.5.8", "@types/node-fetch@^2.6.4": +"@types/node-fetch@2.6.11", "@types/node-fetch@^2.6.4": version "2.6.11" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== @@ -19599,16 +19599,11 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@5.7.2, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== - semver@^6.0.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"