Compare commits
806 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cf36f049a | |||
| c6248b0c72 | |||
| 2a890cf2b3 | |||
| a8c611911d | |||
| 8363f06a5c | |||
| 5f040092cb | |||
| 347c748dd9 | |||
| f234a1f892 | |||
| 0c5e87b7e7 | |||
| 64082ceac0 | |||
| f598e8dab1 | |||
| 0c84a3ff9e | |||
| c8760b5e9c | |||
| 34360a5b8f | |||
| 5e6debcce0 | |||
| c191786e50 | |||
| 6911e37c18 | |||
| 86b2132cdc | |||
| 25f0dba83a | |||
| daa032e92c | |||
| ae65ec97d6 | |||
| e7e4302100 | |||
| 7e7d35980a | |||
| 5001dda284 | |||
| a75d35bdc0 | |||
| 2e631194e1 | |||
| f39892dc6e | |||
| 6bcfd87851 | |||
| 4addb50ca4 | |||
| 161ab3fe13 | |||
| 4f4626a163 | |||
| 82d5f02ea0 | |||
| 05aaeddf22 | |||
| 949ab2b7dc | |||
| f12d3ca59c | |||
| 42147d4d5b | |||
| 7850358039 | |||
| 52cae1e22f | |||
| 48f9fb2609 | |||
| ce8a4d230b | |||
| 1553573b94 | |||
| 41a09bf5c3 | |||
| 5dbe7afae8 | |||
| 6633cfc869 | |||
| d127367df9 | |||
| b21ec41ffd | |||
| 66292bc0cf | |||
| b946b245c2 | |||
| c238147b75 | |||
| 5f33c993f6 | |||
| 0e6d6cd416 | |||
| 199dea5df6 | |||
| 5a3ea9f7d0 | |||
| 9ae49a7175 | |||
| d30d9cfd4e | |||
| a0e7b9b665 | |||
| 1dcb7fb08d | |||
| a7e84b9903 | |||
| 3bebbf9ce9 | |||
| ab8da07518 | |||
| c8bae76121 | |||
| 2873698959 | |||
| 741ebef90e | |||
| 0741f46aca | |||
| 5a33ffad92 | |||
| 562f00dc7e | |||
| e50ab0150e | |||
| fe44b33da2 | |||
| 22e392ba9d | |||
| 4997602e9b | |||
| bdb8d4d89a | |||
| b1d5ee12e4 | |||
| a5e733246d | |||
| 1a08b39b8b | |||
| ba1c3235b3 | |||
| 7c1d2f4554 | |||
| 23a44b2f04 | |||
| 2d3c8e7eb2 | |||
| 2f138acad7 | |||
| 176e53dd7f | |||
| 6a31d06c02 | |||
| f739ba122b | |||
| f53cb687bf | |||
| 42dcf35f45 | |||
| 37dc9a620d | |||
| 9e771a2f24 | |||
| b19e9d23ba | |||
| 7e3a9d634b | |||
| 8c14c330f5 | |||
| 3022ec2acd | |||
| cb9528d894 | |||
| 97cb41dc5a | |||
| 8985c8604f | |||
| 2f95aa0c4f | |||
| 5d0e96163f | |||
| 5e98fdb2e9 | |||
| 23298bbe85 | |||
| cc48ac8fd3 | |||
| 2d9909577e | |||
| 3838bdc54d | |||
| c7d8b137ca | |||
| 3a0df0b491 | |||
| 2331205c93 | |||
| 224d3600b2 | |||
| 7d469038bf | |||
| 1c78397f18 | |||
| 73eff20d08 | |||
| 7e3aaa8c30 | |||
| 95536902f5 | |||
| cf206e6220 | |||
| 4662cc8a26 | |||
| 8c52556cc0 | |||
| b1da877dc7 | |||
| d8cef11a25 | |||
| ef5c0a371e | |||
| b2b7855114 | |||
| 129efc33b9 | |||
| 6a40c66a7b | |||
| 2806177c72 | |||
| 4408bdb0d3 | |||
| 871a6b46c8 | |||
| fd39cab845 | |||
| 1cee679397 | |||
| a7a78702b1 | |||
| 26d887c1a6 | |||
| ceb490c60a | |||
| 1849b4d39f | |||
| 9516ac21bb | |||
| 0e173137e8 | |||
| 1202ef3e80 | |||
| 2ef0b81743 | |||
| 5a5450d7ef | |||
| 6f1f10ccff | |||
| 5ff7861968 | |||
| f1a1c358bc | |||
| efd19353e9 | |||
| a29a493678 | |||
| 27b179276d | |||
| 155cf48a17 | |||
| 358ad79ae4 | |||
| c1fcdf7065 | |||
| 1fe58e21be | |||
| 3c0e7ba4d5 | |||
| e8f159d894 | |||
| 168d7103b1 | |||
| 63d7250a70 | |||
| 88f21e2186 | |||
| d9a18b0c42 | |||
| df588b8845 | |||
| 5be504ce8f | |||
| 6268e327b9 | |||
| 1c81b3a23c | |||
| d28895ce9c | |||
| 1a23287863 | |||
| 0b3ba65df3 | |||
| c3a7cd6a39 | |||
| 39e6c15adf | |||
| aec6673b07 | |||
| 9e8cb1c755 | |||
| cdf02f7c3f | |||
| 77779a4648 | |||
| e16ec3f388 | |||
| 63cfc9ec19 | |||
| 4827117163 | |||
| 5777ef3532 | |||
| 80a3b04493 | |||
| 7c5fdd3b19 | |||
| 31fd8e5200 | |||
| b6a01a52e2 | |||
| f9563fca5f | |||
| af1da58718 | |||
| 0f83ceaf01 | |||
| aecf9575b1 | |||
| 773229ce4c | |||
| da02ecd113 | |||
| 967e92e2c8 | |||
| d7af73d9ed | |||
| c5045ad82e | |||
| 8c22968bc9 | |||
| 8aad271ffe | |||
| b28573455a | |||
| e25725ea8b | |||
| 1f8065143a | |||
| 8e32314667 | |||
| 0de55e74a8 | |||
| e5c5ec18d3 | |||
| f9f6cb74ad | |||
| 026e24bf84 | |||
| ec1890c275 | |||
| 046f1176eb | |||
| 04c5b67475 | |||
| 7b5f797c95 | |||
| 14f9472250 | |||
| efede83271 | |||
| 904db95f59 | |||
| 525999440a | |||
| 6e5805977a | |||
| eb897a2a23 | |||
| 05d997a52d | |||
| f8cde5d627 | |||
| 1b55254834 | |||
| 6436e3fda8 | |||
| 680273770c | |||
| 75e77de466 | |||
| 75cbcd8e85 | |||
| 701f24cece | |||
| 166160dda9 | |||
| 9c5116f197 | |||
| deb1f043c7 | |||
| cbba19a9b1 | |||
| a5daa76616 | |||
| 08b8e73393 | |||
| 2f3cf9825a | |||
| b0f02e726d | |||
| ac40292813 | |||
| 2ac00ee8cc | |||
| 972f9e35fd | |||
| 1d7ee163a0 | |||
| 1054ec0a5e | |||
| fc2e76db0a | |||
| ef3d541eb3 | |||
| 8daf3313f2 | |||
| 93c913ec5a | |||
| 71b2fa8e21 | |||
| f2b78e0f32 | |||
| b70ef80320 | |||
| 426d42ab63 | |||
| 242cefd35c | |||
| 6beda1575a | |||
| ba75d52d29 | |||
| 35a4361ae9 | |||
| 77225383a4 | |||
| f0e81c0cf0 | |||
| 9355d7bf27 | |||
| cd39227af7 | |||
| 14e21214af | |||
| 03184b0321 | |||
| cd5af769ab | |||
| d116670318 | |||
| 9ba5ebd4bd | |||
| 3f1f06f7c5 | |||
| a78586d2fa | |||
| 8e93b0af25 | |||
| 9a5d50d933 | |||
| 1189a94bff | |||
| f128f7acd2 | |||
| 80ec2d1186 | |||
| 18e6f0d7c4 | |||
| 48f2511764 | |||
| c20ea88021 | |||
| ce2b2b9606 | |||
| f223766445 | |||
| 3dfb49e63f | |||
| a13d3dad93 | |||
| fa9695873a | |||
| 70ce1d5553 | |||
| 9974ce24cd | |||
| 93e9c19e0d | |||
| 1694cca92c | |||
| c1d9276dd5 | |||
| f5753a0f5a | |||
| 843f5a968d | |||
| e09b6bbc16 | |||
| 19d6b40bdb | |||
| 1f2706a063 | |||
| aaeadd13cf | |||
| 72e3eb11d7 | |||
| 6f55d8a6c9 | |||
| 70846aefbe | |||
| 47ba90f191 | |||
| cee3c9c070 | |||
| 15f3b9625c | |||
| d34a1f0fd8 | |||
| 0e3b3bcf9a | |||
| 50c91f976b | |||
| 50599eccd2 | |||
| 9f2db10548 | |||
| 5dce1a40bc | |||
| b75b69c3b1 | |||
| e6070e6efd | |||
| 3918664945 | |||
| b65b3fa6d2 | |||
| 162693e19e | |||
| becd734ea8 | |||
| 9dfc8083f0 | |||
| 7bd98c23a4 | |||
| 6f19d78e53 | |||
| 163baa752f | |||
| 6f708df6f6 | |||
| eb5c62cca1 | |||
| 99d2debca8 | |||
| 74e59dccdb | |||
| 812b17a221 | |||
| 32bf0f53f6 | |||
| 17f80baf48 | |||
| e0377a7244 | |||
| b7e5723fb2 | |||
| 897210d5e3 | |||
| ee3d38fce8 | |||
| 37abd3fe47 | |||
| b509ee5b1f | |||
| dd2b59efe5 | |||
| 5788785b3e | |||
| e488ad19b3 | |||
| cbc03c898c | |||
| 155bfbdc42 | |||
| 5d3714a4fc | |||
| a8a1d210d8 | |||
| 49de8b63a7 | |||
| 03c9f51b47 | |||
| 8d00fd3966 | |||
| 80fd21354e | |||
| a87b15825a | |||
| 6a8f98c469 | |||
| 8685298430 | |||
| d43af2db57 | |||
| ef718c48e4 | |||
| 45e7373279 | |||
| 6af2d6e021 | |||
| 9b36f36f73 | |||
| 3a0dbf057e | |||
| dae84ab48a | |||
| aa927597f3 | |||
| ef7f0fb54a | |||
| 324320fdc8 | |||
| 4b27d080b6 | |||
| 7918063753 | |||
| e2202c2417 | |||
| 9817c8f8c0 | |||
| 1d8c04d0b4 | |||
| 10762f10bf | |||
| da7c0d4ff2 | |||
| 4a27973de1 | |||
| bdeb087e28 | |||
| 3662cd58f5 | |||
| 62c52bca07 | |||
| 1fc4a06133 | |||
| 147862e647 | |||
| 12bc323e57 | |||
| de8f9ef587 | |||
| 217402050d | |||
| de75d55849 | |||
| c4e6215b29 | |||
| 57315595a5 | |||
| fc7ed6bd20 | |||
| 750927dc8d | |||
| 00d2ae0a7c | |||
| 56c4d68bae | |||
| a02cdf71c0 | |||
| 25030debe4 | |||
| 27243c4432 | |||
| 22559c59d4 | |||
| 700948bfbb | |||
| 960a39fbf4 | |||
| d986f438d9 | |||
| c2012bd9e3 | |||
| 1f0f92371f | |||
| 4b81725e80 | |||
| 846eaf569d | |||
| ccf1ddf2fd | |||
| 81e00993f2 | |||
| e22c7698c3 | |||
| ef053fc2f7 | |||
| 3b6d3a4631 | |||
| 31c3e603ed | |||
| 1dfe5c980d | |||
| 80e4dc0941 | |||
| bc0b83aa14 | |||
| 971c6c66f0 | |||
| 343697dc93 | |||
| 4058231d6b | |||
| c5ff254267 | |||
| ac14cb9624 | |||
| bcabc50ad1 | |||
| cbd7bce106 | |||
| 00aa8a09fa | |||
| d04f958c11 | |||
| 5716fc31cf | |||
| a5d628af0c | |||
| 111a1f95ba | |||
| 4bf74d86b3 | |||
| d2638d64e4 | |||
| 02cddeec4d | |||
| a8fe8b5f1e | |||
| 8d2db7ffcd | |||
| 313590905f | |||
| 7bbdab7dd9 | |||
| 304832ecde | |||
| 0ab4abfcfe | |||
| 1847822f31 | |||
| 4499495212 | |||
| 0035070db8 | |||
| 890a97d071 | |||
| 5441e5b07f | |||
| ab4b94f2d8 | |||
| fd7ccb4a5b | |||
| 2e6bf234d5 | |||
| c934b0ecdd | |||
| cd6d6f24eb | |||
| e632a08557 | |||
| 0057b4a3f1 | |||
| ac8c1e333e | |||
| 321297b685 | |||
| 67e67d6a5d | |||
| 4385e0b9c9 | |||
| 02243242f1 | |||
| 1af42b766d | |||
| 9d6ec3758b | |||
| 9e78ff4d1c | |||
| 2037aef080 | |||
| 48b9cf042f | |||
| 1866e5e78d | |||
| bc1b3800c8 | |||
| a2505c838b | |||
| bffa863fb4 | |||
| d00d7677bc | |||
| 3ae73be562 | |||
| 763080cd4d | |||
| 6ff06b21a8 | |||
| adebde1b43 | |||
| 89922fe394 | |||
| f219ebb8f8 | |||
| eace97ec7a | |||
| ec1df8b958 | |||
| 8d2d8ef979 | |||
| ae15f3cdc5 | |||
| 3c41b40c5f | |||
| 3b38756dda | |||
| 5a4494ba85 | |||
| 02ad852dd4 | |||
| 7ce1097826 | |||
| 7ee0de5341 | |||
| 0db249871b | |||
| 87df656470 | |||
| 54f07c4330 | |||
| ae061cb69a | |||
| ba529def34 | |||
| 210a85c463 | |||
| 3ad3ad088f | |||
| 617da68ab5 | |||
| efceb930e5 | |||
| 836fa52757 | |||
| 902edf4a85 | |||
| 0e770655dc | |||
| db922a67e2 | |||
| 048a9c96ea | |||
| ed93cf313a | |||
| 97a0e17192 | |||
| 022a58e142 | |||
| e0149c1cbf | |||
| 4665f9c2f7 | |||
| 9ab6b30aea | |||
| e5d2457717 | |||
| 486d89ecf4 | |||
| 94b1f63c7c | |||
| 0299463f62 | |||
| f7ccb6bb51 | |||
| 6c6f5c2c0f | |||
| 1695b4dc68 | |||
| 1ed08df198 | |||
| c300fc18e1 | |||
| bea7e2bb2b | |||
| ad7f658f15 | |||
| f23adfa45c | |||
| 20ee391525 | |||
| 78184b23b7 | |||
| e204f671be | |||
| 8c90f5043f | |||
| 7a322406da | |||
| 307f0ffd34 | |||
| 5f8a5c4995 | |||
| 3747dbc1a8 | |||
| 20859abda9 | |||
| 9ebb3decdb | |||
| 35347e8d63 | |||
| e37d2ce50f | |||
| 49e923026e | |||
| f1a8db9649 | |||
| dddeefd544 | |||
| b4aaa8966f | |||
| ad9db4edd3 | |||
| fef9ed4fcc | |||
| 9ec47eda6e | |||
| 32edf3404c | |||
| bcf80f9d77 | |||
| df137402cf | |||
| daddb7f7b7 | |||
| 82e8b9d66a | |||
| 765d5d97cf | |||
| ed4d3ba265 | |||
| a358ec0f17 | |||
| 524721002a | |||
| 9ab54edbf5 | |||
| 8a7f8c8ebc | |||
| 3ce539af2a | |||
| 3158311aa1 | |||
| 6b4d96dddd | |||
| dc3a32d2bc | |||
| bda01c886e | |||
| 93916653be | |||
| 40f8976350 | |||
| 78e905a735 | |||
| 126ff61738 | |||
| 1e63238cac | |||
| 6ffe34a642 | |||
| 69f276765b | |||
| 75554071a7 | |||
| a1fab10bfd | |||
| 5bc286145d | |||
| 44508ce8b4 | |||
| 90105a199a | |||
| 9618bec847 | |||
| 66a4e27fdd | |||
| ef906f43d7 | |||
| 93b2a1aece | |||
| 050629d281 | |||
| abe9012f20 | |||
| ad2ef3b53f | |||
| 1e1f547ce1 | |||
| 92a34cf135 | |||
| 1e5081f4a2 | |||
| 6f9e13948d | |||
| 55e34b5f9a | |||
| dd47d2b7e7 | |||
| 445cc9e845 | |||
| 637454f73d | |||
| e9dda817e5 | |||
| e811a82be2 | |||
| fd9bf0120c | |||
| e5f8a99bff | |||
| 7305edc398 | |||
| a69c5f354c | |||
| 0eaec1f88f | |||
| cdd021e76d | |||
| 05d01e0863 | |||
| 2aac4cb06d | |||
| 1c6adbd2fd | |||
| d24d5c1365 | |||
| 138c6523d2 | |||
| d8c1c38123 | |||
| 4d419c0877 | |||
| 50b603f668 | |||
| e1e8410ba4 | |||
| 4d359445a7 | |||
| 8d68a4554d | |||
| 6f34e1006b | |||
| 1fc6f9b3ea | |||
| a1ad1d47a4 | |||
| 9281c0b51d | |||
| 4365c19498 | |||
| 8728c1450b | |||
| a98c14406d | |||
| ab8bfbcd59 | |||
| f2913971db | |||
| a7ae8598db | |||
| 17a7d035bf | |||
| 6facd6a666 | |||
| 79bcc179ba | |||
| 3790681bec | |||
| c32203c759 | |||
| ce449408f0 | |||
| 5600235ba1 | |||
| a8ea1080f3 | |||
| 36223ff49f | |||
| 3ce5b42e05 | |||
| c6c1e4a5d7 | |||
| 84b73fc02a | |||
| cd8e014f08 | |||
| 2e9ee698e9 | |||
| d06b5af538 | |||
| 24a3823d3b | |||
| 8ad260d28a | |||
| 0726c04f20 | |||
| b66eac3c9f | |||
| 310dde4465 | |||
| aefa4cbf44 | |||
| c8c2a05724 | |||
| fb9fdf6a38 | |||
| 9473880d2c | |||
| 473f50b493 | |||
| 4c08482258 | |||
| c603f29197 | |||
| 063ae69409 | |||
| 6711cdaf4f | |||
| bf8ed8d017 | |||
| d1c58712ae | |||
| e74b2daee7 | |||
| 4872cd0a48 | |||
| 1025347c08 | |||
| 43988ea12b | |||
| 984dcb0181 | |||
| b991627cb7 | |||
| cff73a65a5 | |||
| 269091986a | |||
| 7fdda1c969 | |||
| 97691eba05 | |||
| 3f3cef7d6f | |||
| e0f2830072 | |||
| 3cd6cd2f55 | |||
| 0aeec12ccd | |||
| 56775f621e | |||
| 14a99ce27b | |||
| e090bde8bb | |||
| 629204810f | |||
| fdd81354cc | |||
| 4827b295fa | |||
| a21fdc8481 | |||
| c1a4f04d37 | |||
| 48fe6a980c | |||
| 31d47ed1e5 | |||
| c8fe0bbcba | |||
| c72fff6bb6 | |||
| 36062458fe | |||
| 5c86914cda | |||
| b2b115c329 | |||
| 93977ebc48 | |||
| 5ad69b458d | |||
| 4320ad26a4 | |||
| bab50ea7db | |||
| 5d4fe8e795 | |||
| 7c93f6232d | |||
| 97e3e8f435 | |||
| 44d5859844 | |||
| 722e045b20 | |||
| ad6804019b | |||
| cd40a6e55e | |||
| 08d9e24871 | |||
| 0649678dca | |||
| a00ec2d02a | |||
| 54a74ccc2a | |||
| 6f3ab274bd | |||
| 90870ecfb6 | |||
| b640e0e181 | |||
| c41fe824cf | |||
| 62c8319d70 | |||
| b1d23467a2 | |||
| 9a62daf047 | |||
| b9ab1c6fd7 | |||
| c1054711b0 | |||
| a4d4f29a37 | |||
| 2793aadea0 | |||
| e7cfb7d9d9 | |||
| 256a851c9b | |||
| 4ab9c81cb8 | |||
| 2f13d6c89e | |||
| f1f7bad25e | |||
| a8b52a48f7 | |||
| cd0999b2ab | |||
| b4c60f3f04 | |||
| 5cefad3666 | |||
| 7a6ecd8c34 | |||
| ba96ecfd91 | |||
| 7bed3757f2 | |||
| e2e755b1d2 | |||
| a77d23109d | |||
| 0eb3b8d382 | |||
| 0021185288 | |||
| 94c72ff4f6 | |||
| e4c08dc9b2 | |||
| ec74af0d62 | |||
| b6d72dd082 | |||
| 31c311daf0 | |||
| e9b3cc6090 | |||
| 543b04a097 | |||
| d39b85b6e6 | |||
| bbe107bb2b | |||
| c616240db1 | |||
| 166be66ce3 | |||
| 4180d0a337 | |||
| 78108e316c | |||
| 3bb8b70915 | |||
| 1c4f13a0ed | |||
| 3ca0abad53 | |||
| 030c8f897f | |||
| 68175cfa3b | |||
| fd8f757851 | |||
| 876d111c61 | |||
| 730cb95c04 | |||
| 3c388d4fb5 | |||
| 2e23718d73 | |||
| 89ea8c470b | |||
| 178640bfc4 | |||
| 4ed38340e0 | |||
| 69eb9a5eff | |||
| 351425127a | |||
| 95f2ba37cc | |||
| c522860d28 | |||
| 4e561cf01f | |||
| fb93063b47 | |||
| 24d3bf05ca | |||
| a4b15970de | |||
| 2a5b053d97 | |||
| 02c30c9681 | |||
| 2de7221653 | |||
| 2498e59156 | |||
| f3b4ea0155 | |||
| aebf949883 | |||
| 33ac05ebee | |||
| 421183fe55 | |||
| b66c19769d | |||
| 7b8445597f | |||
| 0b719c485f | |||
| 17deeddb87 | |||
| 2546159a94 | |||
| 2f26f20db5 | |||
| 11e0215530 | |||
| ea54953298 | |||
| d21f132674 | |||
| c7b2f0e89c | |||
| a1505606cf | |||
| 9edff55bd5 | |||
| e287215179 | |||
| 776649c997 | |||
| 9de5e0ac48 | |||
| c7d99073f6 | |||
| 5c1694d802 | |||
| 7cd319a6b3 | |||
| 2c9b6918e6 | |||
| a50cfad7ef | |||
| 66cd1decde | |||
| c9bf6d9812 | |||
| 8557f64382 | |||
| 8e994fb0df | |||
| 5c47a834b5 | |||
| f356afef43 | |||
| eba5252e40 | |||
| 72c4ad789a | |||
| 3118c20719 | |||
| 3754c374c3 | |||
| dd6d74121c | |||
| 6fb42b989e | |||
| 82dc1bc56b | |||
| d2c7b34142 | |||
| 918f8d1b08 | |||
| fcbcf70e01 | |||
| 5c6b06a0d4 | |||
| fc4c1bf22e | |||
| 84fac739f0 | |||
| ef73c87e2f | |||
| 26ee5768a5 | |||
| 27dcf06c73 | |||
| d48b2ae506 | |||
| 14a7c04d3b | |||
| abd9b127db | |||
| 0a80d6db31 | |||
| 977c99e587 | |||
| fe8bbd5003 | |||
| c6e38a2b4a | |||
| 56f18fe71c | |||
| 0b9306de82 | |||
| 3b1e6cf33d | |||
| dda219ebbe | |||
| 8bb771e5c6 | |||
| 8723bcbd33 | |||
| 0dcea4f2d7 | |||
| 696c016c90 | |||
| eed6373dbf | |||
| ac61f257c0 | |||
| 8d8625aa77 | |||
| 1e1aa4bca5 | |||
| 1f0cde6d0d | |||
| c4b66a9ed3 | |||
| 133b3aa819 | |||
| 9566fadbbb | |||
| 62570899e8 | |||
| c1f25f8dba | |||
| 37a91026d8 | |||
| c62ce213f5 | |||
| 529751578d | |||
| dce87c3a81 | |||
| a94b10cb0f | |||
| 6817263123 | |||
| ddd629d7db | |||
| 9d2a3d5a12 | |||
| 4bc271208e | |||
| dbd535dd6c | |||
| 57b7fe214a | |||
| b136e2856a | |||
| 03e68c03a9 | |||
| 44048a889d | |||
| f72db62e08 | |||
| 333f2623a5 | |||
| 3ebb3451e4 | |||
| 73afbacdd7 | |||
| d08452d613 | |||
| 4ebe6ede59 | |||
| 09a32e7612 | |||
| d81f5fcfe1 | |||
| 0a6542df76 | |||
| b2eb140ee2 | |||
| e28f692d3d | |||
| dc73e7e637 | |||
| b81e3a4ba5 | |||
| 6fc7b15d23 | |||
| 99dd4b4d5c | |||
| b60b1beeec | |||
| eab916f410 | |||
| 069a003787 | |||
| 9b08080b7a | |||
| 8e7ab54d39 | |||
| 4a16378786 | |||
| e5b30770ce | |||
| 8465b04c89 | |||
| 718a6ad2b7 | |||
| 6ca4bfc363 | |||
| 4859026989 |
@@ -0,0 +1,5 @@
|
|||||||
|
# CodeGraph data files — local to each machine, not for committing.
|
||||||
|
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||||
|
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Ignore config files
|
# Ignore formatter config files
|
||||||
.prettier*
|
.oxfmt*
|
||||||
|
|
||||||
# Ignore install stuff
|
# Ignore install stuff
|
||||||
**/yarn.lock
|
**/yarn.lock
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
# Ignore build folders
|
# Ignore build folders
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
**/node_modules
|
||||||
|
|
||||||
# Ignore default volumes created by running docker compose up
|
# Ignore default volumes created by running docker compose up
|
||||||
ontime-db
|
ontime-db
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
"ONTIME_VERSION.js"
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2020
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"es6": true,
|
|
||||||
"jest": true
|
|
||||||
},
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "eslint-config-prettier"],
|
|
||||||
"plugins": ["@typescript-eslint", "prettier"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["e2e/**/**.spec.ts", "e2e/**/**.test.ts"],
|
|
||||||
"extends": ["plugin:playwright/playwright-test"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"no-useless-concat": "warn",
|
|
||||||
"prefer-template": "warn",
|
|
||||||
"no-throw-literal": "error",
|
|
||||||
"no-console": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"allow": ["warn", "error"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"argsIgnorePattern": "^_",
|
|
||||||
"caughtErrorsIgnorePattern": "^_",
|
|
||||||
"destructuredArrayIgnorePattern": "^_",
|
|
||||||
"varsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prettier/prettier": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"endOfLine": "auto"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
github: cpvalente
|
github: cpvalente
|
||||||
buy_me_a_coffee: cpvalente
|
buy_me_a_coffee: cpvalente
|
||||||
|
|||||||
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 149 KiB |
@@ -1,115 +0,0 @@
|
|||||||
name: Ontime build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: [ "*" ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_macos:
|
|
||||||
runs-on: macOS-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v3
|
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build project packages
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Electron - Build app
|
|
||||||
env:
|
|
||||||
APPLE_ID: ${{ secrets.APPLEID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
run: pnpm dist-mac --env-mode=loose
|
|
||||||
timeout-minutes: 60
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
./apps/electron/dist/ontime-macOS-x64.dmg
|
|
||||||
./apps/electron/dist/ontime-macOS-arm64.dmg
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
build_windows:
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v3
|
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build project packages
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Electron - Build app
|
|
||||||
run: pnpm dist-win
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: './apps/electron/dist/ontime-win64.exe'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
build_ubuntu:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v3
|
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build project packages
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Electron - Build app
|
|
||||||
run: pnpm dist-linux
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: './apps/electron/dist/ontime-linux.AppImage'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -12,21 +12,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up QEMU
|
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
|
||||||
uses: docker/setup-qemu-action@v3
|
- uses: actions/setup-node@v6
|
||||||
|
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: '.nvmrc'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
version: 9
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -38,16 +43,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Copy server
|
- name: Copy server
|
||||||
run: mkdir -p apps/cli/server && cp apps/server/dist/index.cjs apps/cli/server/index.cjs
|
run: mkdir -p apps/cli/server && cp apps/server/dist/index.cjs apps/cli/server/index.cjs
|
||||||
|
|
||||||
- name: Copy client
|
- name: Copy client
|
||||||
run: cp -R apps/client/build apps/cli/client
|
run: cp -R apps/client/build apps/cli/client
|
||||||
|
|
||||||
- name: Copy external
|
- name: Copy external
|
||||||
run: cp -R apps/server/src/external apps/cli/external
|
run: cp -R apps/server/src/external apps/cli/external
|
||||||
|
|
||||||
|
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
|
||||||
|
- name: Install newer version of npm
|
||||||
|
run: npm install -g npm@11
|
||||||
|
|
||||||
- name: Publish to NPM
|
- name: Publish to NPM
|
||||||
run: pnpm publish --access public --no-git-checks
|
run: pnpm publish --access public --no-git-checks
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
NPM_CONFIG_PROVENANCE: true
|
NPM_CONFIG_PROVENANCE: true
|
||||||
working-directory: ./apps/cli
|
working-directory: ./apps/cli
|
||||||
|
|||||||
@@ -6,43 +6,62 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
publish_docker:
|
publish_docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Docker Login
|
- name: Setup pnpm
|
||||||
uses: docker/login-action@v2.1.0
|
uses: pnpm/action-setup@v6
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Docker Setup Buildx
|
- name: Get pnpm store directory
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build and push stable release
|
- name: Setup pnpm cache
|
||||||
if: github.event.release.prerelease == false
|
uses: actions/cache@v5
|
||||||
uses: docker/build-push-action@v4.0.0
|
with:
|
||||||
with:
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
context: .
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
file: ./Dockerfile
|
restore-keys: |
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
${{ runner.os }}-pnpm-store-
|
||||||
# Push is a shorthand for --output=type=registry
|
|
||||||
push: true
|
|
||||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
|
||||||
|
|
||||||
- name: Build and push pre-release
|
- name: Install dependencies
|
||||||
if: github.event.release.prerelease == true
|
run: pnpm install --frozen-lockfile
|
||||||
uses: docker/build-push-action@v4.0.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
# Push is a shorthand for --output=type=registry
|
|
||||||
push: true
|
|
||||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ github.event.release.tag_name }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:nightly
|
|
||||||
|
|
||||||
|
- name: Build project packages
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
run: pnpm build:docker
|
||||||
|
|
||||||
|
- name: Docker Login
|
||||||
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Docker Setup Buildx
|
||||||
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
|
|
||||||
|
- name: Setup Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/ontime
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern=v{{version}}
|
||||||
|
type=semver,pattern=v{{major}}
|
||||||
|
type=raw,value=nightly,enable=${{ github.event.release.prerelease == true }}
|
||||||
|
|
||||||
|
- name: Build and push stable release
|
||||||
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
# Push is a shorthand for --output=type=registry
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
name: Ontime build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_macos:
|
||||||
|
runs-on: macOS-26
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Electron - Build app
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.APPLEID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
run: pnpm dist-mac --env-mode=loose
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
- name: Upload macOS build artifacts
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ontime-macos-build
|
||||||
|
path: |
|
||||||
|
./apps/electron/dist/ontime-macOS-x64.dmg
|
||||||
|
./apps/electron/dist/ontime-macOS-arm64.dmg
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
./apps/electron/dist/ontime-macOS-x64.dmg
|
||||||
|
./apps/electron/dist/ontime-macOS-arm64.dmg
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
build_windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Electron - Build app
|
||||||
|
run: pnpm dist-win
|
||||||
|
|
||||||
|
- name: Upload Windows build artifacts
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ontime-windows-build
|
||||||
|
path: |
|
||||||
|
./apps/electron/dist/ontime-win64.exe
|
||||||
|
./apps/electron/dist/win-unpacked/
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
with:
|
||||||
|
files: './apps/electron/dist/ontime-win64.exe'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
build_ubuntu:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Electron - Build app
|
||||||
|
run: pnpm dist-linux
|
||||||
|
|
||||||
|
- name: Upload Linux build artifacts
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ontime-linux-build
|
||||||
|
path: |
|
||||||
|
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
||||||
|
./apps/electron/dist/ontime-linux-arm64.AppImage
|
||||||
|
./apps/electron/dist/ontime-linux-armv7l.AppImage
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
||||||
|
./apps/electron/dist/ontime-linux-arm64.AppImage
|
||||||
|
./apps/electron/dist/ontime-linux-armv7l.AppImage
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: Ontime Resolver build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_resolver:
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build project packages
|
||||||
|
run: pnpm build:resolver
|
||||||
|
|
||||||
|
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
|
||||||
|
- name: Install newer version of npm
|
||||||
|
run: npm install -g npm@11
|
||||||
|
|
||||||
|
- name: Publish to NPM
|
||||||
|
run: pnpm publish --access public --no-git-checks
|
||||||
|
env:
|
||||||
|
NPM_CONFIG_PROVENANCE: true
|
||||||
|
working-directory: ./apps/resolver
|
||||||
@@ -13,93 +13,99 @@ jobs:
|
|||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
version: 9
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Run code quality per package
|
# Run code quality
|
||||||
- name: React - Run linter + TypeScript checks
|
- name: Run formatter
|
||||||
if: always()
|
run: pnpm format:check
|
||||||
run: pnpm lint && tsc --noEmit
|
|
||||||
working-directory: ./apps/client
|
|
||||||
|
|
||||||
- name: Server - Run linter + TypeScript checks
|
- name: Run linter
|
||||||
if: always()
|
|
||||||
run: pnpm lint && tsc --noEmit
|
|
||||||
working-directory: ./apps/server
|
|
||||||
|
|
||||||
- name: Utils - Run linter + TypeScript checks
|
|
||||||
if: always()
|
|
||||||
run: pnpm lint && tsc --noEmit
|
|
||||||
working-directory: ./packages/utils
|
|
||||||
|
|
||||||
- name: Types - Run linter
|
|
||||||
if: always()
|
|
||||||
run: pnpm lint
|
run: pnpm lint
|
||||||
working-directory: ./packages/types
|
|
||||||
|
- name: Run TypeScript checks
|
||||||
|
run: pnpm typecheck
|
||||||
|
|
||||||
# We choose to run tests separately
|
# We choose to run tests separately
|
||||||
- name: React - Run unit tests
|
- name: Run unit tests
|
||||||
if: always()
|
if: always()
|
||||||
run: pnpm test:pipeline
|
run: pnpm test
|
||||||
working-directory: ./apps/client
|
|
||||||
|
|
||||||
- name: Server - Run unit tests
|
|
||||||
if: always()
|
|
||||||
run: pnpm test:pipeline
|
|
||||||
working-directory: ./apps/server
|
|
||||||
|
|
||||||
- name: Utils - Run unit tests
|
|
||||||
if: always()
|
|
||||||
run: pnpm test:pipeline
|
|
||||||
working-directory: ./packages/utils
|
|
||||||
|
|
||||||
e2e-test:
|
e2e-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
version: 9
|
path: ${{ env.PNPM_STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build client
|
- name: Get installed Playwright version
|
||||||
run: pnpm build:local
|
run: echo "PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --parseable | cut -s -d '@' -f3 | cut -d '/' -f1)" >> $GITHUB_ENV
|
||||||
|
- name: Cache playwright binaries
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/ms-playwright
|
||||||
|
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-playwright-
|
||||||
|
- run: pnpm playwright install --with-deps
|
||||||
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: pnpm playwright install --with-deps
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: pnpm e2e
|
run: pnpm e2e
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: automated-screenshots
|
name: automated-screenshots
|
||||||
path: automated-screenshots/
|
path: automated-screenshots/
|
||||||
|
|||||||
@@ -33,9 +33,16 @@ e2e/tests/fixtures/tmp/*
|
|||||||
# production
|
# production
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# bundled assets
|
||||||
|
translations.json
|
||||||
|
override.css
|
||||||
|
**/external/demo/index.html
|
||||||
|
|
||||||
# working stuff
|
# working stuff
|
||||||
**/TODO.md
|
**/TODO.md
|
||||||
|
**.local.**
|
||||||
|
|
||||||
# docker utils
|
# docker utils
|
||||||
ontime-db
|
ontime-db
|
||||||
@@ -46,4 +53,7 @@ ontime-data/
|
|||||||
**/ONTIME_VERSION.js
|
**/ONTIME_VERSION.js
|
||||||
|
|
||||||
# temporary write files
|
# temporary write files
|
||||||
**.tmp
|
**.tmp
|
||||||
|
|
||||||
|
# Claude Code metadata
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="myCustomPropertiesEnabled" value="true" />
|
|
||||||
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
|
||||||
<option name="myCustomPropertiesList">
|
|
||||||
<value>
|
|
||||||
<list size="3">
|
|
||||||
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
|
|
||||||
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
|
|
||||||
<item index="2" class="java.lang.String" itemvalue="app-region" />
|
|
||||||
</list>
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoredUrls">
|
|
||||||
<list>
|
|
||||||
<option value="http://localhost" />
|
|
||||||
<option value="http://127.0.0.1" />
|
|
||||||
<option value="http://0.0.0.0" />
|
|
||||||
<option value="http://www.w3.org/" />
|
|
||||||
<option value="http://json-schema.org/draft" />
|
|
||||||
<option value="http://java.sun.com/" />
|
|
||||||
<option value="http://xmlns.jcp.org/" />
|
|
||||||
<option value="http://javafx.com/javafx/" />
|
|
||||||
<option value="http://javafx.com/fxml" />
|
|
||||||
<option value="http://maven.apache.org/xsd/" />
|
|
||||||
<option value="http://maven.apache.org/POM/" />
|
|
||||||
<option value="http://www.springframework.org/schema/" />
|
|
||||||
<option value="http://www.springframework.org/tags" />
|
|
||||||
<option value="http://www.springframework.org/security/tags" />
|
|
||||||
<option value="http://www.thymeleaf.org" />
|
|
||||||
<option value="http://www.jboss.org/j2ee/schema/" />
|
|
||||||
<option value="http://www.jboss.com/xml/ns/" />
|
|
||||||
<option value="http://www.ibm.com/webservices/xsd" />
|
|
||||||
<option value="http://activemq.apache.org/schema/" />
|
|
||||||
<option value="http://schema.cloudfoundry.org/spring/" />
|
|
||||||
<option value="http://schemas.xmlsoap.org/" />
|
|
||||||
<option value="http://cxf.apache.org/schemas/" />
|
|
||||||
<option value="http://primefaces.org/ui" />
|
|
||||||
<option value="http://tiles.apache.org/" />
|
|
||||||
<option value="http://__IP__" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"sortPackageJson": false,
|
||||||
|
"sortImports": {},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"build",
|
||||||
|
"coverage",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"playwright-report",
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
"**/*.toml",
|
||||||
|
"**/*.json",
|
||||||
|
"!tsconfig.common.json",
|
||||||
|
"!turbo.json",
|
||||||
|
"!package.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"categories": {
|
||||||
|
"correctness": "warn",
|
||||||
|
"suspicious": "off",
|
||||||
|
"pedantic": "off",
|
||||||
|
"perf": "warn",
|
||||||
|
"style": "off",
|
||||||
|
"restriction": "off",
|
||||||
|
"nursery": "off"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"caughtErrorsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"caughtErrors": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"apps/client/build/**",
|
||||||
|
"apps/electron/assets/**",
|
||||||
|
"**/ONTIME_VERSION.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
build
|
|
||||||
coverage
|
|
||||||
dist
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
playwright-report
|
|
||||||
|
|
||||||
**/*.toml
|
|
||||||
**/*.yml
|
|
||||||
**/*.json
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
Ontime consists of 3 distinct parts
|
Ontime consists of 3 distinct parts
|
||||||
|
|
||||||
- __client__: A React app for Ontime's UI and web clients
|
- **client**: A React app for Ontime's UI and web clients
|
||||||
- __electron__: An electron app which facilitates the cross-platform distribution of Ontime
|
- **electron**: An electron app which facilitates the cross-platform distribution of Ontime
|
||||||
- __server__: A node application which handles the domains services and integrations
|
- **server**: A node application which handles the domains services and integrations
|
||||||
|
|
||||||
The steps below will assume you have locally installed the necessary dependencies.
|
The steps below will assume you have locally installed the necessary dependencies.
|
||||||
Other dependencies will be installed as part of the setup
|
Other dependencies will be installed as part of the setup
|
||||||
|
|
||||||
- __node__ (~20)
|
- **node** (~22)
|
||||||
- __pnpm__ (~9)
|
- **pnpm** (~10)
|
||||||
- __docker__ (only necessary to run and build docker images)
|
- **docker** (only necessary to run and build docker images)
|
||||||
|
|
||||||
## LOCAL DEVELOPMENT
|
## LOCAL DEVELOPMENT
|
||||||
|
|
||||||
@@ -21,18 +21,21 @@ Locally, we would need to run both the React client and the node.js server in de
|
|||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- **Install the project dependencies** by running `pnpm i`
|
||||||
- __Run dev mode__ by running `turbo dev`
|
- **Create a local build** by running `pnpm build`, this will populate local dependencies
|
||||||
|
- **Run dev mode** by running `pnpm dev` or `pnpm dev:electron` to get the electron window
|
||||||
|
|
||||||
### Debugging backend
|
### Debugging backend
|
||||||
|
|
||||||
To debug backend code in Node.js:
|
The previous command will start the development servers for both the client, server and electron applications.
|
||||||
|
Typically in dev mode we prefer to start these in separate terminals to help with error tracking and debugging.
|
||||||
|
|
||||||
- Open two separate terminals and navigate to the `apps/client` and `apps/server` directories.
|
We do that by creating two terminals an running
|
||||||
- In each terminal, run the command `pnpm dev` to start the development servers for both the client and server
|
|
||||||
applications.
|
- **Run the React UI** by running `pnpm dev --filter=ontime-ui`
|
||||||
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by
|
- **Run the nodejs server** by running `pnpm dev --filter=ontime-server`
|
||||||
running `pnpm dev:inspect`.
|
|
||||||
|
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm dev:inspect --filter=ontime-server`.
|
||||||
|
|
||||||
## TESTING
|
## TESTING
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ Generally we have 2 types of tests.
|
|||||||
|
|
||||||
Unit tests are contained in mostly all the apps and packages (client, server and utils)
|
Unit tests are contained in mostly all the apps and packages (client, server and utils)
|
||||||
|
|
||||||
You can run unit tests by running turbo `turbo test:pipeline` from the project root.
|
You can run unit tests by running `pnpm test:pipeline` from the project root.
|
||||||
This will run all tests and close test runner.
|
This will run all tests and close test runner.
|
||||||
|
|
||||||
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
|
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
|
||||||
@@ -65,7 +68,7 @@ start the webserver with `pnpm dev:server`
|
|||||||
|
|
||||||
Some other useful commands
|
Some other useful commands
|
||||||
|
|
||||||
- `pnpm e2e --ui` open playwright UI
|
- `pnpm e2e:ui` open playwright UI
|
||||||
- `pnpm e2e --headed` run tests with a visible browser window
|
- `pnpm e2e --headed` run tests with a visible browser window
|
||||||
|
|
||||||
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
|
||||||
@@ -75,15 +78,12 @@ You can generate a distribution for your OS by running the following steps.
|
|||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- **Install the project dependencies** by running `pnpm i`
|
||||||
- __Build the UI and server__ by running `turbo build:electron`
|
- **Build the UI and server** by running `pnpm build`
|
||||||
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
|
- **Create the package** by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
|
||||||
|
|
||||||
The build distribution assets will be at `.apps/electron/dist`
|
The build distribution assets will be at `.apps/electron/dist`
|
||||||
|
|
||||||
Note: The MacOS build will only work in CI, locally it will fail due to notarisation issues.
|
|
||||||
Use the `turbo dist-mac:local` command to build a MacOS distribution locally.
|
|
||||||
|
|
||||||
## DOCKER
|
## DOCKER
|
||||||
|
|
||||||
Ontime provides a docker-compose file to aid with building and running docker images.
|
Ontime provides a docker-compose file to aid with building and running docker images.
|
||||||
@@ -91,17 +91,31 @@ While it should allow for a generic setup, it might need to be modified to fit y
|
|||||||
|
|
||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Build docker image from__ by running `docker build -t getontime/ontime .`
|
- **Build docker image from** by running `docker build -t getontime/ontime .`
|
||||||
- __Run docker image from compose__ by running `docker-compose up -d`
|
- **Run docker image from compose** by running `docker-compose up -d`
|
||||||
|
|
||||||
Other useful commands
|
Other useful commands
|
||||||
|
|
||||||
- __List running processes__ by running `docker ps`
|
- **List running processes** by running `docker ps`
|
||||||
- __Kill running process__ by running `docker kill <process-id>`
|
- **Kill running process** by running `docker kill <process-id>`
|
||||||
|
|
||||||
## General Info
|
## FORMATTING AND LINTING
|
||||||
|
|
||||||
# APP Building
|
Ontime uses [oxfmt](https://oxc.rs/docs/guide/usage/formatter.html) for formatting and [oxlint](https://oxc.rs/docs/guide/usage/linter.html) for linting. Look for an [oxc extension](https://oxc.rs/docs/guide/usage/linter/editors) for your editor of choice.
|
||||||
|
|
||||||
We build the app from app.js for almost all applications. The output file will still be named index.cjs. This is because of Electron.
|
- **Run formatter** by running `pnpm format`
|
||||||
Building the app from index.ts only applies for applications that don't use electron. index.ts will take over the initialization of the server and UI when electron isn't present.
|
- **Run linter** by running `pnpm lint`
|
||||||
|
|
||||||
|
## CONTRIBUTION GUIDELINES
|
||||||
|
|
||||||
|
If you want to propose changes to the codebase, please reach out before opening a Pull Request.
|
||||||
|
|
||||||
|
For new PRs, please follow the following checklist:
|
||||||
|
|
||||||
|
- [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic
|
||||||
|
- [ ] You have ran code formatting and linting in all your changes
|
||||||
|
- [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages
|
||||||
|
- [ ] The PR body contains description and motivation for the changes
|
||||||
|
|
||||||
|
After this checklist is complete, you can request a review from one of the maintainers to get feedback and approval on the changes. \
|
||||||
|
We will review as soon as possible
|
||||||
|
|||||||
@@ -1,37 +1,30 @@
|
|||||||
FROM node:20-bullseye AS builder
|
ARG NODE_VERSION=22.22.2
|
||||||
ENV PNPM_HOME="/pnpm"
|
FROM node:${NODE_VERSION}-alpine
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
|
||||||
RUN npm install -g pnpm@9.5.0
|
# Set environment variables
|
||||||
COPY . /app
|
# Environment Variable to signal that we are running production
|
||||||
WORKDIR /app
|
ENV NODE_ENV=docker
|
||||||
RUN pnpm --filter=ontime-ui --filter=ontime-server --filter=ontime-utils install --config.dedupe-peer-dependents=false --frozen-lockfile
|
# Ontime Data path
|
||||||
RUN pnpm --filter=ontime-ui --filter=ontime-server run build:docker
|
ENV ONTIME_DATA=/data/
|
||||||
|
|
||||||
FROM node:20-alpine
|
RUN mkdir /app
|
||||||
|
WORKDIR /app/
|
||||||
# Set environment variables
|
|
||||||
# Environment Variable to signal that we are running production
|
# Prepare UI
|
||||||
ENV NODE_ENV=docker
|
COPY apps/client/build/ ./client/
|
||||||
# Ontime Data path
|
|
||||||
ENV ONTIME_DATA=/data/
|
# Prepare Backend
|
||||||
|
COPY apps/server/dist/ ./server/
|
||||||
WORKDIR /app/
|
COPY apps/server/src/external/ ./external/
|
||||||
|
COPY apps/server/src/user/ ./user/
|
||||||
# Prepare UI
|
COPY apps/server/src/html/ ./html/
|
||||||
COPY --from=builder /app/apps/client/build ./client/
|
|
||||||
|
# Export default ports
|
||||||
# Prepare Backend
|
EXPOSE 4001/tcp 8888/udp 9999/udp
|
||||||
COPY --from=builder /app/apps/server/dist/ ./server/
|
|
||||||
COPY --from=builder /app/apps/server/src/external/ ./external/
|
CMD ["node", "server/docker.cjs"]
|
||||||
COPY --from=builder /app/apps/server/src/user/ ./user/
|
|
||||||
COPY --from=builder /app/apps/server/src/html/ ./html/
|
# Build and run commands
|
||||||
|
# pnpm build:docker
|
||||||
# Export default ports
|
# docker buildx build . -t getontime/ontime
|
||||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ getontime/ontime
|
||||||
|
|
||||||
CMD ["node", "server/docker.cjs"]
|
|
||||||
|
|
||||||
# Build and run commands
|
|
||||||
# !!! Note that this command needs pre-build versions of the UI and server apps
|
|
||||||
# docker buildx build . -t getontime/ontime
|
|
||||||
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ getontime/ontime
|
|
||||||
|
|||||||
@@ -7,27 +7,31 @@
|
|||||||
[](https://www.buymeacoffee.com/cpvalente)
|
[](https://www.buymeacoffee.com/cpvalente)
|
||||||
|
|
||||||
## Download the latest release
|
## Download the latest release
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="32"></a>
|
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="32"></a>
|
||||||
|
|
||||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe">Windows</a>
|
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe">Windows</a>
|
||||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">MacOS Arm</a>
|
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">macOS (Apple Silicon)</a>
|
||||||
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">MacOS Intel</a>
|
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">macOS (Intel)</a>
|
||||||
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage">Linux</a>
|
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-x86_64.AppImage">Linux (Intel / AMD 64-bit)</a>
|
||||||
|
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-arm64.AppImage">Linux (ARM 64-bit, Raspberry Pi 4+)</a>
|
||||||
|
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-armv7l.AppImage">Linux (ARM 32-bit, older Raspberry Pi)</a>
|
||||||
|
|
||||||
... or
|
... or
|
||||||
|
|
||||||
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
|
||||||
- Install from <a href="https://www.npmjs.com/package/ontime">NPM</a>
|
- Install from <a href="https://www.npmjs.com/package/@getontime/cli">NPM</a>
|
||||||
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
|
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
|
||||||
|
|
||||||
## Need help?
|
## Need help?
|
||||||
|
|
||||||
We do our best to have most topics covered by the documentation. However, if your question is not covered, you are welcome to [fill in a bug report in an issue](https://github.com/cpvalente/ontime/issues), [ask a question in GitHub discussions](https://github.com/cpvalente/ontime/discussions) or hop in the [discord server](https://discord.com/invite/eje3CSUEXm) for a chat.
|
We do our best to have most topics covered by the documentation. However, if your question is not covered, you are welcome to [fill in a bug report in an issue](https://github.com/cpvalente/ontime/issues), [ask a question in GitHub discussions](https://github.com/cpvalente/ontime/discussions) or hop in the [discord server](https://discord.com/invite/eje3CSUEXm) for a chat.
|
||||||
|
|
||||||
## Using Ontime?
|
## Using Ontime?
|
||||||
|
|
||||||
Let us know!
|
Let us know!
|
||||||
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
|
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
|
||||||
|
|
||||||
We would also like to include a testimonials section in our ✨new website✨. It would be great to showcase the diversity of users running Ontime.
|
|
||||||
|
|
||||||
# Ontime
|
# Ontime
|
||||||
|
|
||||||
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
|
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
|
||||||
@@ -43,12 +47,13 @@ Ontime is made by entertainment and broadcast engineers and used by
|
|||||||
- Houses of worship
|
- Houses of worship
|
||||||
|
|
||||||
## Main features
|
## Main features
|
||||||
- [x] **Multiplatform**: Available for Windows / MacOS, Linux. You can also self host with the docker image
|
|
||||||
- [x] **In any device**: Ontime is available in the local network to any device with a browser, eg: tablets, mobile phones, laptops, signage, media servers...
|
- **Multiplatform**: Available as a Cloud service and for Windows, macOS, Linux, or self-hosted via Docker.
|
||||||
- [x] **Made for teams**: Ontime caters to different roles in your production team: directors, operators, backstage and front of house signage...
|
- **In any device**: Ontime is available to any device with a browser, eg: tablets, mobile phones, laptops, signage, media servers...
|
||||||
- [x] **Delay workflows**: Manage and communicate runtime delays in real-time to your team
|
- **Team Collaboration**: Dedicated views for directors, operators, backstage, and signage.
|
||||||
- [x] **Automatable**: Ontime can be fully or partially controlled by an operator, or run standalone with the system clock
|
- **Real-Time Updates**: Manage and communicate runtime delays effortlessly.
|
||||||
- [x] **Focus on integrations**: Use one of the APIs provided (OSC, HTTP, Websocket) or the available [Companion module](https://bitfocus.io/connections/getontime-ontime) to integrate into your workflow (vMix, disguise, Qlab, OBS)
|
- **Automatable**: Ontime can be fully or partially controlled by an operator, or run standalone with the system clock
|
||||||
|
- **Flexible Integrations**: Use one of the APIs provided (OSC, HTTP, Websocket) or the available [Companion module](https://bitfocus.io/connections/getontime-ontime) to integrate into your workflow (vMix, disguise, Qlab, OBS)
|
||||||
|
|
||||||
... and a lot more ...
|
... and a lot more ...
|
||||||
|
|
||||||
@@ -79,84 +84,29 @@ Ontime is made by video engineers and entertainment technicians.
|
|||||||
|
|
||||||
## Using Ontime
|
## Using Ontime
|
||||||
|
|
||||||
Ontime can be started by downloading the latest release for your platform. \
|
### Getting started
|
||||||
Alternatively you can also use the docker image, available at [Docker Hub](https://hub.docker.com/r/getontime/ontime)
|
|
||||||
|
|
||||||
Once installed and running, any device in the network has access to Ontime.
|
The easiest way to start with Ontime is by leveraging our [Cloud service](https://getontime.no). \
|
||||||
|
This will give you immediate access to running instances of Ontime which are available to share with anyone with an internet connection.
|
||||||
|
|
||||||
Ontime provides different screens which allow for different types of interactions with the data. These are called
|
Alternatively, you can run Ontime locally for free by downloading the latest release for your platform or using the docker image, available at [Docker Hub](https://hub.docker.com/r/getontime/ontime)
|
||||||
views. \
|
|
||||||
Each view in Ontime focuses on empowering a specific role or achieving a particular task.
|
|
||||||
|
|
||||||
You can access the different views by reaching the ontime server, in your browser, at (_default port
|
Once installed and running, any device that shares the same network as Ontime will have access to Ontime.
|
||||||
4001_) `http://localhost:4001` or `http://192.168.1.3:4001`
|
|
||||||
|
|
||||||
```
|
|
||||||
For the backstage views
|
|
||||||
-------------------------------------------------------------
|
|
||||||
IP.ADDRESS:4001/timer > Presenter / Stage timer view
|
|
||||||
IP.ADDRESS:4001/minimal > Simple timer view
|
|
||||||
IP.ADDRESS:4001/clock > Simple clock view
|
|
||||||
IP.ADDRESS:4001/backstage > Stage Manager / Backstage view
|
|
||||||
IP.ADDRESS:4001/countdown > Countdown to anything
|
|
||||||
IP.ADDRESS:4001/studio > Studio Clock
|
|
||||||
IP.ADDRESS:4001/timeline > Timeline
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
For the public views
|
|
||||||
-------------------------------------------------------------
|
|
||||||
IP.ADDRESS:4001/public > Public / Foyer view
|
|
||||||
IP.ADDRESS:4001/lower > Lower Thirds
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
For production views
|
|
||||||
-------------------------------------------------------------
|
|
||||||
IP.ADDRESS:4001/editor > the control interface, same as the app
|
|
||||||
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
|
||||||
IP.ADDRESS:4001/operator > automated views for operators
|
|
||||||
```
|
|
||||||
|
|
||||||
More information is available [in our docs](https://docs.getontime.no)
|
More information is available [in our docs](https://docs.getontime.no)
|
||||||
|
|
||||||
## Roadmap
|
## Continued development
|
||||||
|
|
||||||
### Continued development
|
|
||||||
|
|
||||||
Ontime is under active development. We continue adding and improving features in collaboration with users.
|
Ontime is under active development. We continue adding and improving features in collaboration with users.
|
||||||
|
|
||||||
Have an idea? Reach out via [email](mail@getontime.no)
|
Have an idea? Reach out via [email](mail@getontime.no)
|
||||||
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
||||||
|
|
||||||
### Issues
|
## Issues
|
||||||
|
|
||||||
We use Github's issue tracking for bug reporting and feature requests. \
|
We use Github's issue tracking for bug reporting and feature requests. \
|
||||||
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
|
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
|
||||||
|
|
||||||
#### Unsigned App
|
|
||||||
|
|
||||||
When installing the app you would see warning screens from the Operating System like:
|
|
||||||
|
|
||||||
in Windows
|
|
||||||
|
|
||||||
`Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.`
|
|
||||||
|
|
||||||
or in Linux
|
|
||||||
|
|
||||||
`Could Not Display "ontime-linux.AppImage`
|
|
||||||
|
|
||||||
We currently only sign MacOS releases. \
|
|
||||||
Purchasing the certificates for both Mac and Windows would mean a recurrent expense which we are not able to cover.
|
|
||||||
|
|
||||||
You can circumvent this by allowing the execution of the app manually.
|
|
||||||
|
|
||||||
- In Windows: click `more` -> `Run Anyway`
|
|
||||||
- In Linux: right-click the AppImage file: `Properties` -> `Permissions` -> `Allow Executing File as a Program`
|
|
||||||
|
|
||||||
If you have tips on how to improve this or would like to sponsor the code signing,
|
|
||||||
please [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
||||||
@@ -166,8 +116,10 @@ If you are a developer and would like to contribute with code, please open an is
|
|||||||
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
- [Ontime website](https://getontime.no)
|
|
||||||
|
- [Ontime website](https://getontime.no)
|
||||||
- [Documentation](https://docs.getontime.no)
|
- [Documentation](https://docs.getontime.no)
|
||||||
|
- [Video tutorials](https://www.youtube.com/@ontimeapp)
|
||||||
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
|
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
# Terms of sponsorship
|
# Terms of sponsorship
|
||||||
|
|
||||||
Ontime does not exist as a fiscal entity. The sponsorship recipient is the project owner. In this light, contributors are funding the project owner's capacity to work on the project, not the project itself. As a contribution recipient, the code owner reserves the right to redistribute.
|
Ontime does not exist as a fiscal entity. The sponsorship recipient is the project owner. In this light, contributors are funding the project owner's capacity to work on the project, not the project itself. As a contribution recipient, the code owner reserves the right to redistribute.
|
||||||
|
|
||||||
## Why sponsor
|
## Why sponsor
|
||||||
|
|
||||||
Ontime is and will remain free, regardless of your decision to contribute.
|
Ontime is and will remain free, regardless of your decision to contribute.
|
||||||
Regardless, you may choose to provide a monetary contribution:
|
Regardless, you may choose to provide a monetary contribution:
|
||||||
|
|
||||||
### Improve user experience
|
### Improve user experience
|
||||||
|
|
||||||
Certain parts of the application user experience are tied to our capacity to pay for services. App signing and Google Sheets integration are good examples of this.
|
Certain parts of the application user experience are tied to our capacity to pay for services. App signing and Google Sheets integration are good examples of this.
|
||||||
|
|
||||||
### Cool stuff
|
### Cool stuff
|
||||||
|
|
||||||
We may choose not to implement some features. This could be related to the complexity required, to the skill set, or to the fact that we just feel it doesn't improve the app for a significant percentage of users. In those cases, a feature may be considered "not worth it," regardless of its pertinence.
|
We may choose not to implement some features. This could be related to the complexity required, to the skill set, or to the fact that we just feel it doesn't improve the app for a significant percentage of users. In those cases, a feature may be considered "not worth it," regardless of its pertinence.
|
||||||
|
|
||||||
As a user, you can choose to sponsor a particular feature or donate to the app so that we can create bounties for features.
|
As a user, you can choose to sponsor a particular feature or donate to the app so that we can create bounties for features.
|
||||||
|
|
||||||
### Priorities
|
### Priorities
|
||||||
|
|
||||||
Monetary contributions are most immediately needed to offset the costs of presenting and distributing Ontime, i.e., hosting, domains, and certificates.
|
Monetary contributions are most immediately needed to offset the costs of presenting and distributing Ontime, i.e., hosting, domains, and certificates.
|
||||||
|
|
||||||
Additional funding will be considered according to the needs at the time.
|
Additional funding will be considered according to the needs at the time.
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended"
|
|
||||||
],
|
|
||||||
"plugins": [],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-var-requires": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
||||||
|
"extends": ["../../.oxlintrc.json"],
|
||||||
|
"plugins": ["unicorn", "typescript", "oxc", "vitest"]
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Ontime CLI
|
# Ontime CLI
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
> Ontime is an application for managing rundown and event timers.
|
> Ontime is an application for managing rundown and event timers.
|
||||||
|
|
||||||
Congratulations! You got this far into Ontime's rabbit hole and want to manage your installation.
|
Congratulations! You got this far into Ontime's rabbit hole and want to manage your installation.
|
||||||
@@ -10,33 +11,40 @@ The CLI tool is a fast and lightweight way of installing Ontime and is perfect f
|
|||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
You will need to have installed the correct version of node.js
|
You will need to have installed the correct version of node.js
|
||||||
Please check [the app engines declaration](https://github.com/cpvalente/ontime/blob/master/package.json) for the correct version.
|
Please check [the app engines declaration](https://github.com/cpvalente/ontime/blob/master/package.json) for the correct version.
|
||||||
|
|
||||||
### Running Ontime
|
### Running Ontime
|
||||||
|
|
||||||
To get Ontime running, all you need to do is install it in your system using your package manager of choice:
|
To get Ontime running, all you need to do is install it in your system using your package manager of choice:
|
||||||
|
|
||||||
Install globally in your system
|
Install globally in your system
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @getontime/cli
|
npm install -g @getontime/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
... and run using the installed script
|
... and run using the installed script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ontime
|
ontime
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install and run ontime (the installation here is temporary for the duration of the session)
|
Or install and run ontime (the installation here is temporary for the duration of the session)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx install @getontime/cli
|
npx install @getontime/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [Ontime's repository](https://github.com/cpvalente/ontime)
|
- [Ontime's repository](https://github.com/cpvalente/ontime)
|
||||||
- [Ontime's documentation](https://docs.getontime.no/)
|
- [Ontime's documentation](https://docs.getontime.no/)
|
||||||
- [Ontime's website](https://getontime.no/)
|
- [Ontime's website](https://getontime.no/)
|
||||||
|
|
||||||
## Sponsoring
|
## Sponsoring
|
||||||
|
|
||||||
You can help the development of this project or say thank you with a one time donation. \
|
You can help the development of this project or say thank you with a one time donation. \
|
||||||
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
|
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.13.1",
|
"version": "4.11.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lighdev",
|
"lighdev",
|
||||||
"ontime",
|
"ontime",
|
||||||
"timer"
|
"timer"
|
||||||
],
|
],
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ontime": "main.js"
|
"ontime": "main.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"client",
|
"client",
|
||||||
"external",
|
"external",
|
||||||
"server"
|
"server"
|
||||||
],
|
]
|
||||||
"devDependencies": {
|
}
|
||||||
"eslint": "catalog:",
|
|
||||||
"eslint-config-prettier": "catalog:",
|
|
||||||
"prettier": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
build
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
{
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:@tanstack/eslint-plugin-query/recommended"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"react",
|
|
||||||
"simple-import-sort",
|
|
||||||
"@tanstack/query"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react/jsx-no-bind": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"ignoreRefs": true,
|
|
||||||
"allowArrowFunctions": true,
|
|
||||||
"allowFunctions": false,
|
|
||||||
"allowBind": false,
|
|
||||||
"ignoreDOMComponents": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"react/jsx-boolean-value": [
|
|
||||||
"error",
|
|
||||||
"never",
|
|
||||||
{
|
|
||||||
"always": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"react/jsx-handler-names": [
|
|
||||||
"off",
|
|
||||||
{
|
|
||||||
"eventHandlerPrefix": "handle",
|
|
||||||
"eventHandlerPropPrefix": "on"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"react/no-danger": "warn",
|
|
||||||
"react/no-deprecated": [
|
|
||||||
"error"
|
|
||||||
],
|
|
||||||
"react/jsx-uses-react": "off",
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"react/jsx-no-undef": "error",
|
|
||||||
"react/jsx-closing-tag-location": "warn",
|
|
||||||
"react/jsx-no-useless-fragment": "warn",
|
|
||||||
"react/no-unescaped-entities": "error",
|
|
||||||
"react/jsx-tag-spacing": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"closingSlash": "never",
|
|
||||||
"beforeSelfClosing": "always",
|
|
||||||
"afterOpening": "never",
|
|
||||||
"beforeClosing": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"react/jsx-space-before-closing": [
|
|
||||||
"off",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"react/jsx-curly-brace-presence": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"props": "never",
|
|
||||||
"children": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"react/destructuring-assignment": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"react/no-unsafe": "off",
|
|
||||||
"react/prop-types": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"ignore": [],
|
|
||||||
"customValidators": [],
|
|
||||||
"skipUndeclared": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"simple-import-sort/imports": "warn",
|
|
||||||
"simple-import-sort/exports": "warn"
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
// override "simple-import-sort" config
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"*.js",
|
|
||||||
"*.jsx",
|
|
||||||
"*.ts",
|
|
||||||
"*.tsx"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"simple-import-sort/imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"groups": [
|
|
||||||
// Packages `react` related packages come first.
|
|
||||||
[
|
|
||||||
"^react",
|
|
||||||
"^@?\\w"
|
|
||||||
],
|
|
||||||
// Internal packages.
|
|
||||||
[
|
|
||||||
"^(@|components)(/.*|$)"
|
|
||||||
],
|
|
||||||
// Side effect imports.
|
|
||||||
[
|
|
||||||
"^\\u0000"
|
|
||||||
],
|
|
||||||
// Parent imports. Put `..` last.
|
|
||||||
[
|
|
||||||
"^\\.\\.(?!/?$)",
|
|
||||||
"^\\.\\./?$"
|
|
||||||
],
|
|
||||||
// Other relative imports. Put same-folder imports and `.` last.
|
|
||||||
[
|
|
||||||
"^\\./(?=.*/)(?!/?$)",
|
|
||||||
"^\\.(?!/?$)",
|
|
||||||
"^\\./?$"
|
|
||||||
],
|
|
||||||
// Style imports.
|
|
||||||
[
|
|
||||||
"^.+\\.?(css)$"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
||||||
|
"extends": ["../../.oxlintrc.json"],
|
||||||
|
"plugins": ["unicorn", "typescript", "oxc", "react", "vitest"]
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
@@ -3,26 +3,28 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<meta name="theme-color" content="#101010" />
|
<meta name="theme-color" content="#101010" />
|
||||||
<meta name="ontime" content="ontime - time keeping for live events" />
|
<meta name="ontime" content="ontime - time keeping for live events" />
|
||||||
<link rel="apple-touch-icon" href="/ontime-logo.png" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<link rel="icon" type="image/png" href="/ontime-logo.png" />
|
<link rel="apple-touch-icon" href="ontime-logo.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="icon" type="image/png" href="ontime-logo.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="site.webmanifest" />
|
||||||
|
<link rel="manifest" href="manifest.json" />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<title>ontime</title>
|
<title>ontime</title>
|
||||||
<style>
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
background-color: rgba(0, 0, 0, 0) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
if (/\/timer\/?$/.test(window.location.pathname) && typeof Promise.any !== 'function') {
|
||||||
|
var b = document.querySelector('base');
|
||||||
|
var base = b ? (b.getAttribute('href') || '').replace(/\/$/, '') : '';
|
||||||
|
window.location.replace(base + '/timer-legacy' + window.location.search);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,58 +1,57 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.13.1",
|
"version": "4.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.7.0",
|
"@base-ui/react": "1.6.0",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@codemirror/commands": "^6.0.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@codemirror/lang-css": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@fontsource/open-sans": "^5.2.7",
|
||||||
"@emotion/react": "^11.10.6",
|
"@mantine/hooks": "^8.3.7",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@sentry/react": "^10.59.0",
|
||||||
"@fontsource/open-sans": "^5.0.28",
|
"@table-nav/react": "^0.0.7",
|
||||||
"@mantine/hooks": "^7.13.3",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@tanstack/react-query-devtools": "^5.101.0",
|
||||||
"@sentry/react": "^8.43.0",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tanstack/react-query": "^5.62.7",
|
"@uiw/codemirror-theme-vscode": "^4.25.10",
|
||||||
"@tanstack/react-query-devtools": "^5.62.7",
|
|
||||||
"@tanstack/react-table": "^8.20.5",
|
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.18.0",
|
||||||
"color": "^4.2.3",
|
"csv-stringify": "^6.6.0",
|
||||||
"csv-stringify": "^6.4.5",
|
"qrcode": "^1.5.4",
|
||||||
"framer-motion": "^10.10.0",
|
"react": "^19.2.7",
|
||||||
"react": "^18.3.1",
|
"react-colorful": "^5.7.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-dom": "^19.2.7",
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"react-fast-compare": "^3.2.2",
|
"react-fast-compare": "^3.2.2",
|
||||||
"react-hook-form": "^7.53.1",
|
"react-hook-form": "^7.80.0",
|
||||||
"react-qr-code": "^2.0.12",
|
"react-icons": "5.6.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router": "^8.0.1",
|
||||||
"web-vitals": "^3.1.1",
|
"react-virtuoso": "^4.18.7",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
"postinstall": "pnpm addversion",
|
"postinstall": "pnpm addversion",
|
||||||
"dev": "cross-env BROWSER=none vite",
|
"dev": "cross-env BROWSER=none vite",
|
||||||
|
"dev:electron": "pnpm dev",
|
||||||
|
"lint": "oxlint --quiet --type-aware",
|
||||||
|
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:local": "cross-env NODE_ENV=local vite build",
|
|
||||||
"build:electron": "cross-env NODE_ENV=local vite build",
|
|
||||||
"build:docker": "cross-env VITE_IS_DOCKER=true vite build",
|
|
||||||
"build:localdocker": "cross-env NODE_ENV=local vite build",
|
|
||||||
"lint": "eslint . --quiet",
|
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:pipeline": "vitest run",
|
"test:pipeline": "vitest run",
|
||||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build",
|
|
||||||
"analyse": "npx vite-bundle-visualizer"
|
"analyse": "npx vite-bundle-visualizer"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
"not dead",
|
"not dead",
|
||||||
"not op_mini all"
|
"not op_mini all",
|
||||||
|
"safari >= 14.1"
|
||||||
],
|
],
|
||||||
"development": [
|
"development": [
|
||||||
"last 1 chrome version",
|
"last 1 chrome version",
|
||||||
@@ -61,31 +60,21 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/vite-plugin": "^2.16.1",
|
"@sentry/vite-plugin": "5.1.1",
|
||||||
"@tanstack/eslint-plugin-query": "^5.8.4",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/color": "^3.0.3",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@vitejs/plugin-legacy": "^8.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
"@vitejs/plugin-react": "5.2.0",
|
||||||
"@typescript-eslint/parser": "catalog:",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"happy-dom": "^20.9.0",
|
||||||
"eslint": "catalog:",
|
|
||||||
"eslint-config-prettier": "catalog:",
|
|
||||||
"eslint-plugin-jest": "^28.6.0",
|
|
||||||
"eslint-plugin-prettier": "catalog:",
|
|
||||||
"eslint-plugin-react": "^7.32.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
|
||||||
"happy-dom": "^16.7.2",
|
|
||||||
"ontime-types": "workspace:*",
|
"ontime-types": "workspace:*",
|
||||||
"ontime-utils": "workspace:*",
|
"ontime-utils": "workspace:*",
|
||||||
"prettier": "catalog:",
|
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "^5.2.11",
|
"vite": "8.0.1",
|
||||||
"vite-plugin-compression2": "^1.3.3",
|
"vite-plugin-compression2": "2.5.1",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "4.5.0",
|
||||||
"vite-tsconfig-paths": "^4.3.1",
|
|
||||||
"vitest": "catalog:"
|
"vitest": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scope": "/",
|
"scope": "./",
|
||||||
"start_url": "/",
|
"start_url": "./",
|
||||||
"display": "",
|
"display": "",
|
||||||
"theme_color": "#121212",
|
"theme_color": "#121212",
|
||||||
"background_color": "#ffffff"
|
"background_color": "#101010"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "",
|
"name": "",
|
||||||
"short_name": "",
|
"short_name": "",
|
||||||
"icons": [
|
"icons": [{ "src": "ontime-logo.png", "sizes": "295x295", "type": "image/png" }],
|
||||||
{ "src": "/ontime-logo.png", "sizes": "295x295", "type": "image/png" }
|
"theme_color": "#121212",
|
||||||
],
|
|
||||||
"theme_color": "#2B5ABC",
|
|
||||||
"background_color": "#101010",
|
"background_color": "#101010",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,33 @@
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
import { Tooltip } from '@base-ui/react/tooltip';
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
import { BrowserRouter } from 'react-router';
|
||||||
|
|
||||||
|
// apply global axios config defaults
|
||||||
|
import './common/api/axios.config';
|
||||||
|
import AppRouter from './AppRouter';
|
||||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||||
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
||||||
import { AppContextProvider } from './common/context/AppContext';
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
import { ontimeQueryClient } from './common/queryClient';
|
import { ontimeQueryClient } from './common/queryClient';
|
||||||
import { connectSocket } from './common/utils/socket';
|
import { connectSocket } from './common/utils/socket';
|
||||||
import theme from './theme/theme';
|
|
||||||
import { TranslationProvider } from './translation/TranslationProvider';
|
|
||||||
import AppRouter from './AppRouter';
|
|
||||||
import { baseURI } from './externals';
|
import { baseURI } from './externals';
|
||||||
|
import KeepAwake from './features/keep-awake/KeepAwake';
|
||||||
|
import { TranslationProvider } from './translation/TranslationProvider';
|
||||||
|
|
||||||
connectSocket();
|
connectSocket();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ChakraProvider disableGlobalStyle resetCSS theme={theme}>
|
<QueryClientProvider client={ontimeQueryClient}>
|
||||||
<QueryClientProvider client={ontimeQueryClient}>
|
<AppContextProvider>
|
||||||
<AppContextProvider>
|
<Tooltip.Provider>
|
||||||
<BrowserRouter basename={baseURI}>
|
<BrowserRouter basename={baseURI}>
|
||||||
<div className='App'>
|
<div className='App'>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<TranslationProvider>
|
<TranslationProvider>
|
||||||
<IdentifyOverlay />
|
<IdentifyOverlay />
|
||||||
|
<KeepAwake />
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</TranslationProvider>
|
</TranslationProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@@ -34,9 +37,9 @@ function App() {
|
|||||||
<div id='identify-portal' />
|
<div id='identify-portal' />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AppContextProvider>
|
</Tooltip.Provider>
|
||||||
</QueryClientProvider>
|
</AppContextProvider>
|
||||||
</ChakraProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,168 +1,115 @@
|
|||||||
import React from 'react';
|
import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types';
|
||||||
import {
|
import { ComponentType, Suspense, lazy, useEffect, useMemo } from 'react';
|
||||||
createRoutesFromChildren,
|
import { Navigate, Route, useLocation, useNavigate, useParams } from 'react-router';
|
||||||
matchRoutes,
|
|
||||||
Navigate,
|
|
||||||
Route,
|
|
||||||
Routes,
|
|
||||||
useLocation,
|
|
||||||
useNavigationType,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import * as Sentry from '@sentry/react';
|
|
||||||
|
|
||||||
|
import ViewNavigationMenu from './common/components/navigation-menu/ViewNavigationMenu';
|
||||||
|
import { PresetContext } from './common/context/PresetContext';
|
||||||
|
import useUrlPresets from './common/hooks-query/useUrlPresets';
|
||||||
import { useClientPath } from './common/hooks/useClientPath';
|
import { useClientPath } from './common/hooks/useClientPath';
|
||||||
|
import { getRouteFromPreset } from './common/utils/urlPresets';
|
||||||
|
import { getIsNavigationLocked, sessionScope } from './externals';
|
||||||
import Log from './features/log/Log';
|
import Log from './features/log/Log';
|
||||||
import withPreset from './features/PresetWrapper';
|
import { initializeSentry } from './sentry.config';
|
||||||
import withData from './features/viewers/ViewWrapper';
|
import Loader from './views/common/loader/Loader';
|
||||||
|
import NotFound from './views/common/not-found/NotFound';
|
||||||
import ViewLoader from './views/ViewLoader';
|
import ViewLoader from './views/ViewLoader';
|
||||||
import { ONTIME_VERSION } from './ONTIME_VERSION';
|
|
||||||
import { sentryDsn, sentryRecommendedIgnore } from './sentry.config';
|
|
||||||
|
|
||||||
const Editor = React.lazy(() => import('./features/editors/ProtectedEditor'));
|
const Timer = lazy(() => import('./views/timer/Timer'));
|
||||||
const Cuesheet = React.lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
|
const Countdown = lazy(() => import('./views/countdown/Countdown'));
|
||||||
const Operator = React.lazy(() => import('./features/operator/OperatorExport'));
|
const Backstage = lazy(() => import('./views/backstage/Backstage'));
|
||||||
|
const StudioClock = lazy(() => import('./views/studio/Studio'));
|
||||||
|
const Timeline = lazy(() => import('./views/timeline/TimelinePage'));
|
||||||
|
const ProjectInfo = lazy(() => import('./views/project-info/ProjectInfo'));
|
||||||
|
|
||||||
const TimerView = React.lazy(() => import('./views/timer/Timer'));
|
const Editor = lazy(() => import('./views/editor/ProtectedEditor'));
|
||||||
const MinimalTimerView = React.lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
|
const Cuesheet = lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
|
||||||
const ClockView = React.lazy(() => import('./features/viewers/clock/Clock'));
|
const Operator = lazy(() => import('./features/operator/OperatorExport'));
|
||||||
const Countdown = React.lazy(() => import('./features/viewers/countdown/Countdown'));
|
|
||||||
|
|
||||||
const Backstage = React.lazy(() => import('./views/backstage/Backstage'));
|
const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper'));
|
||||||
const Timeline = React.lazy(() => import('./views/timeline/TimelinePage'));
|
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
|
||||||
const Public = React.lazy(() => import('./views/public/Public'));
|
const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport'));
|
||||||
const Lower = React.lazy(() => import('./features/viewers/lower-thirds/LowerThird'));
|
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
|
||||||
const StudioClock = React.lazy(() => import('./features/viewers/studio/StudioClock'));
|
|
||||||
const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo'));
|
|
||||||
|
|
||||||
const STimer = withPreset(withData(TimerView));
|
// Initialize Sentry with our configuration
|
||||||
const SMinimalTimer = withPreset(withData(MinimalTimerView));
|
const SentryRouter = initializeSentry();
|
||||||
const SClock = withPreset(withData(ClockView));
|
|
||||||
const SCountdown = withPreset(withData(Countdown));
|
|
||||||
const SBackstage = withPreset(withData(Backstage));
|
|
||||||
const SProjectInfo = withPreset(withData(ProjectInfo));
|
|
||||||
const SPublic = withPreset(withData(Public));
|
|
||||||
const SLowerThird = withPreset(withData(Lower));
|
|
||||||
const SStudio = withPreset(withData(StudioClock));
|
|
||||||
const STimeline = withPreset(withData(Timeline));
|
|
||||||
const PCuesheet = withPreset(Cuesheet);
|
|
||||||
const POperator = withPreset(Operator);
|
|
||||||
|
|
||||||
const EditorFeatureWrapper = React.lazy(() => import('./features/EditorFeatureWrapper'));
|
|
||||||
const RundownPanel = React.lazy(() => import('./features/rundown/RundownExport'));
|
|
||||||
const TimerControl = React.lazy(() => import('./features/control/playback/TimerControlExport'));
|
|
||||||
const MessageControl = React.lazy(() => import('./features/control/message/MessageControlExport'));
|
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
dsn: sentryDsn,
|
|
||||||
integrations: [
|
|
||||||
Sentry.reactRouterV6BrowserTracingIntegration({
|
|
||||||
useEffect: React.useEffect,
|
|
||||||
useLocation,
|
|
||||||
useNavigationType,
|
|
||||||
createRoutesFromChildren,
|
|
||||||
matchRoutes,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
tracesSampleRate: 0.3,
|
|
||||||
release: ONTIME_VERSION,
|
|
||||||
enabled: import.meta.env.PROD,
|
|
||||||
ignoreErrors: [...sentryRecommendedIgnore, /Unable to preload CSS/i, /dynamically imported module/i],
|
|
||||||
denyUrls: [/extensions\//i, /^chrome:\/\//i, /^chrome-extension:\/\//i],
|
|
||||||
});
|
|
||||||
|
|
||||||
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
|
|
||||||
|
|
||||||
export default function AppRouter() {
|
export default function AppRouter() {
|
||||||
// handle client path changes
|
// handle client path changes
|
||||||
useClientPath();
|
useClientPath();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Suspense fallback={null}>
|
<Suspense fallback={<Loader />}>
|
||||||
<SentryRoutes>
|
<SentryRouter>
|
||||||
<Route path='/' element={<Navigate to='/timer' />} />
|
<Route path='/' element={<Navigate to='/timer' />} />
|
||||||
<Route
|
<Route
|
||||||
path='/timer'
|
path='timer'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<STimer />
|
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
||||||
|
<Timer />
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/public'
|
path='countdown'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<SPublic />
|
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
||||||
|
<Countdown />
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/minimal'
|
path='backstage'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<SMinimalTimer />
|
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
||||||
|
<Backstage />
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/clock'
|
path='studio'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<SClock />
|
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
||||||
|
<StudioClock />
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/countdown'
|
path='timeline'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<SCountdown />
|
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
||||||
|
<Timeline />
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/backstage'
|
path='info'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
<SBackstage />
|
<ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />
|
||||||
|
<ProjectInfo />
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path='/studio'
|
|
||||||
element={
|
|
||||||
<ViewLoader>
|
|
||||||
<SStudio />
|
|
||||||
</ViewLoader>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{/*/!* Lower third cannot have a loading screen *!/*/}
|
|
||||||
<Route path='/lower' element={<SLowerThird />} />
|
|
||||||
<Route
|
|
||||||
path='/timeline'
|
|
||||||
element={
|
|
||||||
<ViewLoader>
|
|
||||||
<STimeline />
|
|
||||||
</ViewLoader>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path='/info'
|
|
||||||
element={
|
|
||||||
<ViewLoader>
|
|
||||||
<SProjectInfo />
|
|
||||||
</ViewLoader>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*/!* Protected Routes *!/*/}
|
{/*/!* Protected Routes *!/*/}
|
||||||
<Route path='/editor' element={<Editor />} />
|
<Route path='editor' element={<Editor />} />
|
||||||
<Route path='/cuesheet' element={<PCuesheet />} />
|
<Route path='cuesheet' element={<Cuesheet />} />
|
||||||
<Route path='/op' element={<POperator />} />
|
<Route
|
||||||
|
path='op'
|
||||||
|
element={
|
||||||
|
<ViewLoader>
|
||||||
|
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
|
||||||
|
<Operator />
|
||||||
|
</ViewLoader>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{/*/!* Protected Routes - Elements *!/*/}
|
{/*/!* Protected Routes - Elements *!/*/}
|
||||||
<Route
|
<Route
|
||||||
path='/rundown'
|
path='rundown'
|
||||||
element={
|
element={
|
||||||
<EditorFeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<RundownPanel />
|
<RundownPanel />
|
||||||
@@ -170,7 +117,7 @@ export default function AppRouter() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/timercontrol'
|
path='timercontrol'
|
||||||
element={
|
element={
|
||||||
<EditorFeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<TimerControl />
|
<TimerControl />
|
||||||
@@ -178,7 +125,7 @@ export default function AppRouter() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/messagecontrol'
|
path='messagecontrol'
|
||||||
element={
|
element={
|
||||||
<EditorFeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<MessageControl />
|
<MessageControl />
|
||||||
@@ -186,16 +133,119 @@ export default function AppRouter() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/log'
|
path='log'
|
||||||
element={
|
element={
|
||||||
<EditorFeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<Log />
|
<Log />
|
||||||
</EditorFeatureWrapper>
|
</EditorFeatureWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/*/!* Send to default if nothing found *!/*/}
|
{/**
|
||||||
<Route path='*' element={<STimer />} />
|
* If the views are prefixed with the "preset" path, we are in a locked preset
|
||||||
</SentryRoutes>
|
* Locked presets do not expose their parameters
|
||||||
</React.Suspense>
|
*/}
|
||||||
|
<Route path='preset/:alias' element={<PresetView />} />
|
||||||
|
|
||||||
|
{/**
|
||||||
|
* If we havent matched any views or presets, we may be in an unlocked preset
|
||||||
|
* Unlocked presets are unwrapped to expose their target and parameters
|
||||||
|
*/}
|
||||||
|
<Route path='*' element={<RedirectPreset />} />
|
||||||
|
</SentryRouter>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PresetViewMap: Record<OntimeViewPresettable, ComponentType> = {
|
||||||
|
[OntimeView.Cuesheet]: Cuesheet,
|
||||||
|
[OntimeView.Operator]: Operator,
|
||||||
|
[OntimeView.Timer]: Timer,
|
||||||
|
[OntimeView.Backstage]: Backstage,
|
||||||
|
[OntimeView.Timeline]: Timeline,
|
||||||
|
[OntimeView.StudioClock]: StudioClock,
|
||||||
|
[OntimeView.Countdown]: Countdown,
|
||||||
|
[OntimeView.ProjectInfo]: ProjectInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This view will mask a configured canonical route
|
||||||
|
* and inject the preset search parameters to context
|
||||||
|
* User are not able to configure the parameters locked presets
|
||||||
|
*/
|
||||||
|
function PresetView() {
|
||||||
|
const { data, status } = useUrlPresets();
|
||||||
|
const { alias } = useParams();
|
||||||
|
|
||||||
|
const preset: URLPreset | undefined = useMemo(() => {
|
||||||
|
if (status === 'pending' || !alias) return;
|
||||||
|
return data.find((p) => p.alias === alias && p.enabled);
|
||||||
|
}, [data, status, alias]);
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to check the session scope to determine if the user can navigate
|
||||||
|
* If the user has a global scope, they can navigate freely
|
||||||
|
* Otherwise, they are locked to the preset view
|
||||||
|
*/
|
||||||
|
const showNav = sessionScope === 'rw';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we are in a preset path but cannot find a preset, we will need to show a not found page
|
||||||
|
* This can happen if the preset was deleted or disabled
|
||||||
|
*/
|
||||||
|
if (!preset) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewNavigationMenu isNavigationLocked={!showNav} suppressSettings />
|
||||||
|
<NotFound />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locked presets do not allow configuration changes
|
||||||
|
* Whether the user can navigate is determined by the locked param
|
||||||
|
*
|
||||||
|
* We inject the preset to the context value for the view to consume
|
||||||
|
*/
|
||||||
|
const Component = PresetViewMap[preset.target as OntimeViewPresettable];
|
||||||
|
return (
|
||||||
|
<PresetContext value={preset}>
|
||||||
|
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSettings />
|
||||||
|
{Component ? <Component /> : <NotFound />}
|
||||||
|
</PresetContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RedirectPreset() {
|
||||||
|
const { data, status } = useUrlPresets();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// checks if we are in a preset path and resolves a destination URL
|
||||||
|
const destination = useMemo(() => {
|
||||||
|
if (status === 'pending') return null;
|
||||||
|
return getRouteFromPreset(location, data);
|
||||||
|
}, [data, location, status]);
|
||||||
|
|
||||||
|
// if we have a destination, we will navigate to it
|
||||||
|
useEffect(() => {
|
||||||
|
if (destination) {
|
||||||
|
navigate(`/${destination}`, { replace: true });
|
||||||
|
}
|
||||||
|
}, [destination, navigate]);
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSettings />
|
||||||
|
<NotFound />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { TranslationObject } from 'ontime-types';
|
||||||
|
|
||||||
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
|
import { TRANSLATION, apiEntryUrl, customTranslationsURL } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
|
const assetsPath = `${apiEntryUrl}/assets`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to get css contents
|
||||||
|
*/
|
||||||
|
export async function getCSSContents(options?: RequestOptions): Promise<string> {
|
||||||
|
const res = await axios.get(`${assetsPath}/css`, { signal: options?.signal });
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to post css contents
|
||||||
|
*/
|
||||||
|
export async function postCSSContents(css: string): Promise<void> {
|
||||||
|
await axios.post(`${assetsPath}/css`, {
|
||||||
|
css,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to restore default css
|
||||||
|
*/
|
||||||
|
export async function restoreCSSContents(): Promise<string> {
|
||||||
|
const res = await axios.post(`${assetsPath}/css/restore`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to get user translation
|
||||||
|
*/
|
||||||
|
export async function getUserTranslation(options?: RequestOptions): Promise<TranslationObject> {
|
||||||
|
const res = await axios.get(customTranslationsURL, { signal: options?.signal });
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to post user translation
|
||||||
|
*/
|
||||||
|
export async function postUserTranslation(translation: TranslationObject): Promise<void> {
|
||||||
|
await axios.post(`${assetsPath}/translations`, {
|
||||||
|
translation,
|
||||||
|
});
|
||||||
|
await ontimeQueryClient.invalidateQueries({ queryKey: TRANSLATION });
|
||||||
|
}
|
||||||
@@ -9,14 +9,15 @@ import type {
|
|||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const automationsPath = `${apiEntryUrl}/automations`;
|
const automationsPath = `${apiEntryUrl}/automations`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to get the automations settings
|
* HTTP request to get the automations settings
|
||||||
*/
|
*/
|
||||||
export async function getAutomationSettings(): Promise<AutomationSettings> {
|
export async function getAutomationSettings(options?: RequestOptions): Promise<AutomationSettings> {
|
||||||
const res = await axios.get(automationsPath);
|
const res = await axios.get(automationsPath, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { axiosConfig } from './requestTimeouts';
|
||||||
|
|
||||||
|
axios.defaults.validateStatus = (status) => {
|
||||||
|
return status >= 200 && status < 300;
|
||||||
|
};
|
||||||
|
axios.defaults.timeout = axiosConfig.shortTimeout;
|
||||||
@@ -3,27 +3,30 @@ import { serverURL } from '../../externals';
|
|||||||
// keys in tanstack store
|
// keys in tanstack store
|
||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
|
export const APP_SERVER_PORT = ['appServerPort'];
|
||||||
export const APP_VERSION = ['appVersion'];
|
export const APP_VERSION = ['appVersion'];
|
||||||
export const AUTOMATION = ['automation'];
|
export const AUTOMATION = ['automation'];
|
||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
|
export const CUSTOM_VIEWS = ['customViews'];
|
||||||
export const PROJECT_DATA = ['project'];
|
export const PROJECT_DATA = ['project'];
|
||||||
export const PROJECT_LIST = ['projectList'];
|
export const PROJECT_LIST = ['projectList'];
|
||||||
|
export const PROJECT_RUNDOWNS = ['projectRundowns'];
|
||||||
export const RUNDOWN = ['rundown'];
|
export const RUNDOWN = ['rundown'];
|
||||||
|
export const CURRENT_RUNDOWN_QUERY_KEY = ['rundown', 'current'];
|
||||||
|
export const getRundownQueryKey = (rundownId: string) => ['rundown', rundownId];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
export const SHEET_STATE = ['sheetState'];
|
|
||||||
export const URL_PRESETS = ['urlpresets'];
|
export const URL_PRESETS = ['urlpresets'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
|
export const CSS_OVERRIDE = ['cssOverride'];
|
||||||
export const CLIENT_LIST = ['clientList'];
|
export const CLIENT_LIST = ['clientList'];
|
||||||
export const REPORT = ['report'];
|
export const REPORT = ['report'];
|
||||||
|
export const TRANSLATION = ['translation'];
|
||||||
|
|
||||||
// API URLs
|
// API URLs
|
||||||
export const apiEntryUrl = `${serverURL}/data`;
|
export const apiEntryUrl = `${serverURL}/data`;
|
||||||
|
|
||||||
export const projectDataURL = `${serverURL}/project`;
|
const userAssetsPath = 'user';
|
||||||
export const rundownURL = `${serverURL}/events`;
|
const customTranslationsPath = 'translations/translations.json';
|
||||||
export const ontimeURL = `${serverURL}/ontime`;
|
|
||||||
|
|
||||||
export const userAssetsPath = 'user';
|
|
||||||
export const cssOverridePath = 'styles/override.css';
|
|
||||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
|
||||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
||||||
|
export const customTranslationsURL = `${serverURL}/${userAssetsPath}/${customTranslationsPath}`;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { CustomField, CustomFieldLabel, CustomFields } from 'ontime-types';
|
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests list of known custom fields
|
* Requests list of known custom fields
|
||||||
*/
|
*/
|
||||||
export async function getCustomFields(): Promise<CustomFields> {
|
export async function getCustomFields(options?: RequestOptions): Promise<CustomFields> {
|
||||||
const res = await axios.get(customFieldsPath);
|
const res = await axios.get(customFieldsPath, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,15 +25,15 @@ export async function postCustomField(newField: CustomField): Promise<CustomFiel
|
|||||||
/**
|
/**
|
||||||
* Edits single custom field
|
* Edits single custom field
|
||||||
*/
|
*/
|
||||||
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
|
export async function editCustomField(key: CustomFieldKey, newField: CustomField): Promise<CustomFields> {
|
||||||
const res = await axios.put(`${customFieldsPath}/${label}`, { ...newField });
|
const res = await axios.put(`${customFieldsPath}/${key}`, { ...newField });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes single custom field
|
* Deletes single custom field
|
||||||
*/
|
*/
|
||||||
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
|
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFields> {
|
||||||
const res = await axios.delete(`${customFieldsPath}/${label}`);
|
const res = await axios.delete(`${customFieldsPath}/${key}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { type CustomViewsListResponse, type MessageResponse } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
import { axiosConfig } from './requestTimeouts';
|
||||||
|
import { downloadBlob } from './utils';
|
||||||
|
|
||||||
|
const customViewsPath = `${apiEntryUrl}/custom-views`;
|
||||||
|
|
||||||
|
export async function getCustomViews(options?: RequestOptions): Promise<CustomViewsListResponse> {
|
||||||
|
const response = await axios.get<CustomViewsListResponse>(customViewsPath, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.shortTimeout,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadCustomView(slug: string, file: File, options?: RequestOptions): Promise<MessageResponse> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('indexHtml', file);
|
||||||
|
|
||||||
|
return (
|
||||||
|
await axios.post<MessageResponse>(`${customViewsPath}/${encodeURIComponent(slug)}/upload`, formData, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadCustomView(slug: string, options?: RequestOptions): Promise<void> {
|
||||||
|
const response = await axios.get(`${customViewsPath}/${encodeURIComponent(slug)}/download`, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadBlob(response.data, `${slug}-index.html`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreDemoView(options?: RequestOptions): Promise<MessageResponse> {
|
||||||
|
return (
|
||||||
|
await axios.post<MessageResponse>(`${customViewsPath}/restore-demo`, null, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCustomView(slug: string, options?: RequestOptions): Promise<void> {
|
||||||
|
await axios.delete(`${customViewsPath}/${encodeURIComponent(slug)}`, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||||
|
|
||||||
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
|
||||||
import { makeCSVFromArrayOfArrays } from '../utils/csv';
|
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
import { axiosConfig } from './requestTimeouts';
|
||||||
import { createBlob, downloadBlob } from './utils';
|
import { createBlob, downloadBlob } from './utils';
|
||||||
|
|
||||||
const dbPath = `${apiEntryUrl}/db`;
|
const dbPath = `${apiEntryUrl}/db`;
|
||||||
@@ -13,7 +12,7 @@ const dbPath = `${apiEntryUrl}/db`;
|
|||||||
* HTTP request to the current DB
|
* HTTP request to the current DB
|
||||||
*/
|
*/
|
||||||
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||||
return axios.post(`${dbPath}/download`, { filename });
|
return axios.post(`${dbPath}/download`, { filename }, { timeout: axiosConfig.longTimeout });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,25 +32,6 @@ export async function downloadProject(fileName: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Request download of the current rundown as a CSV file
|
|
||||||
* @param fileName
|
|
||||||
*/
|
|
||||||
export async function downloadCSV(fileName: string = 'rundown') {
|
|
||||||
try {
|
|
||||||
const { data, name } = await fileDownload(fileName);
|
|
||||||
const { project, rundown, customFields } = data;
|
|
||||||
|
|
||||||
const sheetData = makeTable(project, rundown, customFields);
|
|
||||||
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
|
||||||
|
|
||||||
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
|
||||||
downloadBlob(blob, `${name}.csv`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to upload project file
|
* HTTP request to upload project file
|
||||||
*/
|
*/
|
||||||
@@ -59,6 +39,7 @@ export async function uploadProjectFile(file: File): Promise<MessageResponse> {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('project', file);
|
formData.append('project', file);
|
||||||
const response = await axios.post(`${dbPath}/upload`, formData, {
|
const response = await axios.post(`${dbPath}/upload`, formData, {
|
||||||
|
timeout: axiosConfig.longTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -98,8 +79,11 @@ export async function quickProject(data: QuickStartData): Promise<MessageRespons
|
|||||||
/**
|
/**
|
||||||
* HTTP request to get the list of available project files
|
* HTTP request to get the list of available project files
|
||||||
*/
|
*/
|
||||||
export async function getProjects(): Promise<ProjectFileListResponse> {
|
export async function getProjects(options?: RequestOptions): Promise<ProjectFileListResponse> {
|
||||||
const res = await axios.get(`${dbPath}/all`);
|
const res = await axios.get(`${dbPath}/all`, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout,
|
||||||
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,81 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
import type {
|
||||||
|
SpreadsheetPreviewResponse,
|
||||||
|
SpreadsheetWorksheetMetadata,
|
||||||
|
SpreadsheetWorksheetOptions,
|
||||||
|
} from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
import { axiosConfig } from './requestTimeouts';
|
||||||
|
import { downloadBlob } from './utils';
|
||||||
|
|
||||||
const excelPath = `${apiEntryUrl}/excel`;
|
const excelPath = `${apiEntryUrl}/excel`;
|
||||||
|
|
||||||
type PreviewSpreadsheetResponse = {
|
|
||||||
rundown: OntimeRundown;
|
|
||||||
customFields: CustomFields;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* upload Excel file to server
|
* upload Excel file to server
|
||||||
* @return string - file ID op the uploaded file
|
* Uploads an Excel file and returns worksheet names plus metadata for the initial worksheet.
|
||||||
*/
|
*/
|
||||||
export async function upload(file: File) {
|
export async function upload(file: File, requestOptions?: RequestOptions): Promise<SpreadsheetWorksheetOptions> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('excel', file);
|
formData.append('excel', file);
|
||||||
await axios.post(`${excelPath}/upload`, formData, {
|
const response: AxiosResponse<SpreadsheetWorksheetOptions> = await axios.post(`${excelPath}/upload`, formData, {
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importRundownPreview(
|
||||||
|
options: ImportMap,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
|
): Promise<SpreadsheetPreviewResponse> {
|
||||||
|
const response: AxiosResponse<SpreadsheetPreviewResponse> = await axios.post(
|
||||||
|
`${excelPath}/preview`,
|
||||||
|
{
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorksheetMetadata(
|
||||||
|
worksheet: string,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
|
): Promise<SpreadsheetWorksheetMetadata> {
|
||||||
|
const response: AxiosResponse<SpreadsheetWorksheetMetadata> = await axios.post(
|
||||||
|
`${excelPath}/metadata`,
|
||||||
|
{ worksheet },
|
||||||
|
{
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Worksheet names
|
* Downloads a xlsx representation of the rundown from the server
|
||||||
* @return string[] - array of available worksheets
|
|
||||||
*/
|
*/
|
||||||
export async function getWorksheetNames(): Promise<string[]> {
|
export async function downloadAsExcel(rundownId: string, fileName?: string, requestOptions?: RequestOptions) {
|
||||||
const response: AxiosResponse<string[]> = await axios.get(`${excelPath}/worksheets`);
|
try {
|
||||||
return response.data;
|
const response = await axios.get(`${excelPath}/${rundownId}/export`, {
|
||||||
}
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
downloadBlob(response.data, `${fileName ?? 'Ontime_rundown'}.xlsx`);
|
||||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
|
} catch (error) {
|
||||||
options,
|
console.error('Error downloading file:', error);
|
||||||
});
|
}
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { apiRepoLatest } from '../../externals';
|
import { apiRepoLatest } from '../../externals';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
export type HasUpdate = {
|
export type HasUpdate = {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -10,8 +11,8 @@ export type HasUpdate = {
|
|||||||
/**
|
/**
|
||||||
* HTTP request to get the latest version and url from github
|
* HTTP request to get the latest version and url from github
|
||||||
*/
|
*/
|
||||||
export async function getLatestVersion(): Promise<HasUpdate> {
|
export async function getLatestVersion(options?: RequestOptions): Promise<HasUpdate> {
|
||||||
const res = await axios.get(`${apiRepoLatest}`);
|
const res = await axios.get(apiRepoLatest, { signal: options?.signal });
|
||||||
return {
|
return {
|
||||||
url: res.data.html_url as string,
|
url: res.data.html_url as string,
|
||||||
version: res.data.tag_name as string,
|
version: res.data.tag_name as string,
|
||||||
|
|||||||
@@ -2,22 +2,28 @@ import axios, { AxiosResponse } from 'axios';
|
|||||||
import { ProjectData, ProjectLogoResponse } from 'ontime-types';
|
import { ProjectData, ProjectLogoResponse } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
import { axiosConfig } from './requestTimeouts';
|
||||||
|
|
||||||
const projectPath = `${apiEntryUrl}/project`;
|
const projectPath = `${apiEntryUrl}/project`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch project data
|
* HTTP request to fetch project data
|
||||||
*/
|
*/
|
||||||
export async function getProjectData(): Promise<ProjectData> {
|
export async function getProjectData(options?: RequestOptions): Promise<ProjectData> {
|
||||||
const res = await axios.get(projectPath);
|
const res = await axios.get(projectPath, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeout,
|
||||||
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to mutate project data
|
* HTTP request to mutate project data
|
||||||
*/
|
*/
|
||||||
export async function postProjectData(data: ProjectData): Promise<AxiosResponse<ProjectData>> {
|
export async function postProjectData(data: ProjectData): Promise<ProjectData> {
|
||||||
return axios.post(projectPath, data);
|
const response = await axios.post(projectPath, data);
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +33,7 @@ export async function uploadProjectLogo(file: File): Promise<AxiosResponse<Proje
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', file);
|
formData.append('image', file);
|
||||||
const response = await axios.post(`${projectPath}/upload`, formData, {
|
const response = await axios.post(`${projectPath}/upload`, formData, {
|
||||||
|
timeout: axiosConfig.longTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import axios from 'axios';
|
|||||||
import { OntimeReport } from 'ontime-types';
|
import { OntimeReport } from 'ontime-types';
|
||||||
|
|
||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
|
import { REPORT, apiEntryUrl } from './constants';
|
||||||
import { apiEntryUrl, REPORT } from './constants';
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
export const reportUrl = `${apiEntryUrl}/report`;
|
export const reportUrl = `${apiEntryUrl}/report`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch all reports
|
* HTTP request to fetch all reports
|
||||||
*/
|
*/
|
||||||
export async function fetchReport(): Promise<OntimeReport> {
|
export async function fetchReport(options?: RequestOptions): Promise<OntimeReport> {
|
||||||
const res = await axios.get(`${reportUrl}/`);
|
const res = await axios.get(reportUrl, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type RequestOptions = {
|
||||||
|
signal?: AbortSignal;
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Keep a short global timeout for regular API requests, but allow
|
||||||
|
* longer windows for file transfer and heavy import/export operations.
|
||||||
|
*/
|
||||||
|
export const axiosConfig = {
|
||||||
|
shortTimeout: 20 * 1000, // 20 seconds
|
||||||
|
longTimeout: 3 * 60 * 1000, // 3 minutes
|
||||||
|
} as const;
|
||||||
@@ -1,86 +1,220 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached, TransientEventPayload } from 'ontime-types';
|
import {
|
||||||
|
EntryId,
|
||||||
|
OntimeEntry,
|
||||||
|
OntimeEvent,
|
||||||
|
ProjectRundownsList,
|
||||||
|
RenumberCues,
|
||||||
|
Rundown,
|
||||||
|
RundownImportPayload,
|
||||||
|
TransientEventPayload,
|
||||||
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const rundownPath = `${apiEntryUrl}/rundown`;
|
type RundownId = string;
|
||||||
|
const rundownPath = `${apiEntryUrl}/rundowns`;
|
||||||
|
|
||||||
|
// #region operations on project rundowns =========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to fetch all events
|
* HTTP request to fetch a list of existing rundowns
|
||||||
*/
|
*/
|
||||||
export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
export async function fetchProjectRundownList(options?: RequestOptions): Promise<ProjectRundownsList> {
|
||||||
const res = await axios.get(`${rundownPath}/normalised`);
|
const res = await axios.get(rundownPath, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to post new event
|
* HTTP request to fetch all entries in the currently loaded rundown
|
||||||
*/
|
*/
|
||||||
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function fetchCurrentRundown(options?: RequestOptions): Promise<Rundown> {
|
||||||
return axios.post(rundownPath, data);
|
const res = await axios.get(`${rundownPath}/current`, { signal: options?.signal });
|
||||||
|
if (!isValidRundown(res.data)) {
|
||||||
|
throw new Error('Invalid rundown payload');
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to put new event
|
* HTTP request to fetch all entries in the given rundown
|
||||||
*/
|
*/
|
||||||
export async function requestPutEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function fetchRundown(rundownId: RundownId, options?: RequestOptions): Promise<Rundown> {
|
||||||
return axios.put(rundownPath, data);
|
const res = await axios.get(`${rundownPath}/${rundownId}`, { signal: options?.signal });
|
||||||
|
if (!isValidRundown(res.data)) {
|
||||||
|
throw new Error('Invalid rundown payload');
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BatchEditEntry = {
|
/**
|
||||||
|
* HTTP request to switch the currently loaded rundown
|
||||||
|
*/
|
||||||
|
export async function loadRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||||
|
return axios.post(`${rundownPath}/${rundownId}/load`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to create a new rundown
|
||||||
|
*/
|
||||||
|
export async function createRundown(title: string): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||||
|
return axios.post(rundownPath, { title });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to duplicate an existing rundown
|
||||||
|
*/
|
||||||
|
export async function duplicateRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||||
|
return axios.post(`${rundownPath}/${rundownId}/duplicate`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to rename an existing rundown
|
||||||
|
*/
|
||||||
|
export async function renameRundown(rundownId: RundownId, title: string): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||||
|
return axios.patch(`${rundownPath}/${rundownId}`, { title });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete a rundown
|
||||||
|
*/
|
||||||
|
export async function deleteRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||||
|
return axios.delete(`${rundownPath}/${rundownId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to apply an imported rundown using a merge strategy or into a new rundown
|
||||||
|
*/
|
||||||
|
export async function importRundownWithOptions(
|
||||||
|
payload: RundownImportPayload,
|
||||||
|
): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||||
|
return axios.post(`${rundownPath}/import`, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion operations on project rundowns ======================
|
||||||
|
// #region operations on rundown entries ==========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to post new entry
|
||||||
|
*/
|
||||||
|
export async function postAddEntry(
|
||||||
|
rundownId: RundownId,
|
||||||
|
data: TransientEventPayload,
|
||||||
|
): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
|
return axios.post(`${rundownPath}/${rundownId}/entry`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to edit an entry
|
||||||
|
*/
|
||||||
|
export async function putEditEntry(
|
||||||
|
rundownId: RundownId,
|
||||||
|
data: Partial<OntimeEntry>,
|
||||||
|
): Promise<AxiosResponse<OntimeEntry>> {
|
||||||
|
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BatchEditEntry = {
|
||||||
data: Partial<OntimeEvent>;
|
data: Partial<OntimeEvent>;
|
||||||
ids: string[];
|
ids: EntryId[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to put multiple events
|
* HTTP request to edit multiple events
|
||||||
*/
|
*/
|
||||||
export async function requestBatchPutEvents(data: BatchEditEntry): Promise<AxiosResponse<MessageResponse>> {
|
export async function putBatchEditEvents(rundownId: RundownId, data: BatchEditEntry): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.put(`${rundownPath}/batch`, data);
|
return axios.put(`${rundownPath}/${rundownId}/batch`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to renumber cues for multiple events
|
||||||
|
*/
|
||||||
|
export function patchRenumberCues(rundownId: RundownId, data: RenumberCues): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.patch(`${rundownPath}/${rundownId}/renumber`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReorderEntry = {
|
export type ReorderEntry = {
|
||||||
eventId: string;
|
entryId: EntryId;
|
||||||
from: number;
|
destinationId: EntryId;
|
||||||
to: number;
|
order: 'before' | 'after' | 'insert';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to reorder events
|
* HTTP request to reorder an entry
|
||||||
*/
|
*/
|
||||||
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function patchReorderEntry(rundownId: RundownId, data: ReorderEntry): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.patch(`${rundownPath}/reorder`, data);
|
return axios.patch(`${rundownPath}/${rundownId}/reorder`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SwapEntry = {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to swap two events
|
* HTTP request to swap two events
|
||||||
*/
|
*/
|
||||||
export async function requestEventSwap(data: SwapEntry): Promise<AxiosResponse<MessageResponse>> {
|
export async function requestEventSwap(
|
||||||
return axios.patch(`${rundownPath}/swap`, data);
|
rundownId: RundownId,
|
||||||
|
from: EntryId,
|
||||||
|
to: EntryId,
|
||||||
|
): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to request application of delay
|
* HTTP request to request application of delay
|
||||||
*/
|
*/
|
||||||
export async function requestApplyDelay(eventId: string): Promise<AxiosResponse<MessageResponse>> {
|
export async function requestApplyDelay(rundownId: RundownId, delayId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.patch(`${rundownPath}/applydelay/${eventId}`);
|
return axios.patch(`${rundownPath}/${rundownId}/applydelay/${delayId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to delete given event
|
* HTTP request for cloning an entry
|
||||||
*/
|
*/
|
||||||
export async function requestDelete(eventIds: string[]): Promise<AxiosResponse<MessageResponse>> {
|
export async function postCloneEntry(
|
||||||
return axios.delete(rundownPath, { data: { ids: eventIds } });
|
rundownId: RundownId,
|
||||||
|
entryId: EntryId,
|
||||||
|
options?: { before?: EntryId; after?: EntryId },
|
||||||
|
): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to delete all events
|
* HTTP request for grouping a list of entries into a group
|
||||||
*/
|
*/
|
||||||
export async function requestDeleteAll(): Promise<AxiosResponse<MessageResponse>> {
|
export async function requestGroupEntries(rundownId: RundownId, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.delete(`${rundownPath}/all`);
|
return axios.post(`${rundownPath}/${rundownId}/group`, { ids: entryIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request for dissolving of a group
|
||||||
|
*/
|
||||||
|
export async function requestUngroup(rundownId: RundownId, groupId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.post(`${rundownPath}/${rundownId}/ungroup/${groupId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete entries of a given rundown
|
||||||
|
*/
|
||||||
|
export async function deleteEntries(rundownId: RundownId, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.delete(`${rundownPath}/${rundownId}/entries`, { data: { ids: entryIds } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete all entries of a given rundown
|
||||||
|
*/
|
||||||
|
export async function requestDeleteAll(rundownId: RundownId): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.delete(`${rundownPath}/${rundownId}/all`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion operations on rundown entries =======================
|
||||||
|
|
||||||
|
function isValidRundown(x: any): x is Rundown {
|
||||||
|
return (
|
||||||
|
x &&
|
||||||
|
typeof x === 'object' &&
|
||||||
|
typeof x.id === 'string' &&
|
||||||
|
Array.isArray(x.order) &&
|
||||||
|
Array.isArray(x.flatOrder) &&
|
||||||
|
x.entries &&
|
||||||
|
typeof x.entries === 'object' &&
|
||||||
|
typeof x.revision === 'number'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { GetInfo } from 'ontime-types';
|
import { GetInfo, LinkOptions } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const sessionPath = `${apiEntryUrl}/session`;
|
const sessionPath = `${apiEntryUrl}/session`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve application info
|
* HTTP request to retrieve application info
|
||||||
*/
|
*/
|
||||||
export async function getInfo(): Promise<GetInfo> {
|
export async function getInfo(options?: RequestOptions): Promise<GetInfo> {
|
||||||
const res = await axios.get(`${sessionPath}/info`);
|
const res = await axios.get(`${sessionPath}/info`, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to get a pre-authenticated URL
|
* HTTP request to get a pre-authenticated URL
|
||||||
*/
|
*/
|
||||||
export async function generateUrl(
|
export async function generateUrl(options: LinkOptions & { baseUrl: string; path: string }): Promise<string> {
|
||||||
baseUrl: string,
|
const res = await axios.post(`${sessionPath}/url`, options);
|
||||||
path: string,
|
|
||||||
lock: boolean,
|
|
||||||
authenticate: boolean,
|
|
||||||
): Promise<string> {
|
|
||||||
const res = await axios.post(`${sessionPath}/url`, { baseUrl, path, lock, authenticate });
|
|
||||||
return res.data.url;
|
return res.data.url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { Settings } from 'ontime-types';
|
import { PortInfo, Settings } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const settingsPath = `${apiEntryUrl}/settings`;
|
const settingsPath = `${apiEntryUrl}/settings`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve application settings
|
* HTTP request to retrieve application settings
|
||||||
*/
|
*/
|
||||||
export async function getSettings(): Promise<Settings> {
|
export async function getSettings(options?: RequestOptions): Promise<Settings> {
|
||||||
const res = await axios.get(settingsPath);
|
const res = await axios.get(settingsPath, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,3 +27,18 @@ export async function postSettings(data: Settings): Promise<AxiosResponse<Settin
|
|||||||
export async function postShowWelcomeDialog(show: boolean) {
|
export async function postShowWelcomeDialog(show: boolean) {
|
||||||
axios.post(`${settingsPath}/welcomedialog`, { show });
|
axios.post(`${settingsPath}/welcomedialog`, { show });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to retrieve server port
|
||||||
|
*/
|
||||||
|
export async function getServerPort(options?: RequestOptions): Promise<PortInfo> {
|
||||||
|
const res = await axios.get(`${settingsPath}/serverport`, { signal: options?.signal });
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to set server port
|
||||||
|
*/
|
||||||
|
export async function postServerPort(serverPort: number): Promise<AxiosResponse<PortInfo>> {
|
||||||
|
return axios.post(`${settingsPath}/serverport`, { serverPort });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
import type {
|
||||||
|
AuthenticationStatus,
|
||||||
|
SpreadsheetPreviewResponse,
|
||||||
|
SpreadsheetWorksheetMetadata,
|
||||||
|
SpreadsheetWorksheetOptions,
|
||||||
|
} from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
import { axiosConfig } from './requestTimeouts';
|
||||||
|
|
||||||
const sheetsPath = `${apiEntryUrl}/sheets`;
|
const sheetsPath = `${apiEntryUrl}/sheets`;
|
||||||
|
|
||||||
@@ -23,6 +30,7 @@ export const verifyAuthenticationStatus = async (): Promise<{
|
|||||||
export const requestConnection = async (
|
export const requestConnection = async (
|
||||||
file: File,
|
file: File,
|
||||||
sheetId: string,
|
sheetId: string,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
verification_url: string;
|
verification_url: string;
|
||||||
user_code: string;
|
user_code: string;
|
||||||
@@ -31,6 +39,8 @@ export const requestConnection = async (
|
|||||||
formData.append('client_secret', file);
|
formData.append('client_secret', file);
|
||||||
|
|
||||||
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -53,23 +63,71 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic
|
|||||||
export const previewRundown = async (
|
export const previewRundown = async (
|
||||||
sheetId: string,
|
sheetId: string,
|
||||||
options: ImportMap,
|
options: ImportMap,
|
||||||
): Promise<{
|
requestOptions?: RequestOptions,
|
||||||
rundown: OntimeRundown;
|
): Promise<SpreadsheetPreviewResponse> => {
|
||||||
customFields: CustomFields;
|
const response = await axios.post(
|
||||||
}> => {
|
`${sheetsPath}/${sheetId}/read`,
|
||||||
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
{ options },
|
||||||
|
{
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorksheetNames = async (sheetId: string): Promise<string[]> => {
|
/**
|
||||||
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`);
|
* Fetches derived metadata for a single worksheet by reading its rows and detecting headers.
|
||||||
|
*/
|
||||||
|
export const getWorksheetMetadata = async (
|
||||||
|
sheetId: string,
|
||||||
|
worksheet: string,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
|
): Promise<SpreadsheetWorksheetMetadata> => {
|
||||||
|
const response: AxiosResponse<SpreadsheetWorksheetMetadata> = await axios.post(
|
||||||
|
`${sheetsPath}/${sheetId}/metadata`,
|
||||||
|
{ worksheet },
|
||||||
|
{
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the available worksheets for a Google Sheet. Metadata is loaded lazily per worksheet.
|
||||||
|
*/
|
||||||
|
export const getWorksheetOptions = async (
|
||||||
|
sheetId: string,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
|
): Promise<SpreadsheetWorksheetOptions> => {
|
||||||
|
const response: AxiosResponse<SpreadsheetWorksheetOptions> = await axios.post(
|
||||||
|
`${sheetsPath}/${sheetId}/worksheet-options`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to upload the rundown to a google sheet
|
* HTTP request to upload the rundown to a google sheet
|
||||||
*/
|
*/
|
||||||
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
|
export const uploadRundown = async (
|
||||||
const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options });
|
sheetId: string,
|
||||||
|
options: ImportMap,
|
||||||
|
requestOptions?: RequestOptions,
|
||||||
|
): Promise<void> => {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${sheetsPath}/${sheetId}/write`,
|
||||||
|
{ options },
|
||||||
|
{
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
|
||||||
|
},
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,20 +2,35 @@ import axios from 'axios';
|
|||||||
import { URLPreset } from 'ontime-types';
|
import { URLPreset } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve aliases
|
* HTTP request to retrieve all presets
|
||||||
*/
|
*/
|
||||||
export async function getUrlPresets(): Promise<URLPreset[]> {
|
export async function getUrlPresets(options?: RequestOptions): Promise<URLPreset[]> {
|
||||||
const res = await axios.get(urlPresetsPath);
|
const res = await axios.get(urlPresetsPath, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to mutate aliases
|
* HTTP request to add a preset
|
||||||
*/
|
*/
|
||||||
export async function postUrlPresets(data: URLPreset[]): Promise<URLPreset[]> {
|
export async function postUrlPreset(data: URLPreset): Promise<URLPreset[]> {
|
||||||
return axios.post(urlPresetsPath, data);
|
return (await axios.post(urlPresetsPath, data)).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to edit a preset
|
||||||
|
*/
|
||||||
|
export async function putUrlPreset(alias: string, data: URLPreset): Promise<URLPreset[]> {
|
||||||
|
return (await axios.put(`${urlPresetsPath}/${alias}`, data)).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete a preset
|
||||||
|
*/
|
||||||
|
export async function deleteUrlPreset(alias: string): Promise<URLPreset[]> {
|
||||||
|
return (await axios.delete(`${urlPresetsPath}/${alias}`)).data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { addLog } from '../stores/logger';
|
|||||||
import { nowInMillis } from '../utils/time';
|
import { nowInMillis } from '../utils/time';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility unrwap a potential axios error
|
* Utility unwrap a potential axios error
|
||||||
* @param error
|
* @param error
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@@ -34,7 +34,21 @@ export function maybeAxiosError(error: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility unrwaps a potential axios error and sends to logger
|
* Utility unwrap a an instance of Error
|
||||||
|
*/
|
||||||
|
export function unwrapError(error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
} else {
|
||||||
|
if (typeof error !== 'string') {
|
||||||
|
return JSON.stringify(error);
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility unwraps a potential axios error and sends to logger
|
||||||
* @param prepend
|
* @param prepend
|
||||||
* @param error
|
* @param error
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import type { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
import type { RequestOptions } from './requestOptions';
|
||||||
|
|
||||||
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to retrieve view settings
|
* HTTP request to fetch view settings
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function getView(): Promise<ViewSettings> {
|
export async function getViewSettings(options?: RequestOptions): Promise<ViewSettings> {
|
||||||
const res = await axios.get(viewSettingsPath);
|
const res = await axios.get(viewSettingsPath, { signal: options?.signal });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to mutate view settings
|
* HTTP request to update view settings
|
||||||
|
* needs to update entire objects, not just a patch
|
||||||
*/
|
*/
|
||||||
export async function postViewSettings(data: ViewSettings) {
|
export async function postViewSettings(data: ViewSettings): Promise<ViewSettings> {
|
||||||
return axios.post(viewSettingsPath, data);
|
const res = await axios.post(viewSettingsPath, data);
|
||||||
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
.positioner {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 2px;
|
||||||
|
min-width: var(--anchor-width);
|
||||||
|
max-width: var(--available-width);
|
||||||
|
border: 1px solid $gray-1000;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
background-color: $gray-1200;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&[data-empty][data-hide-empty] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-height: 20rem;
|
||||||
|
max-height: var(--available-height);
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
padding-block: 0.25rem;
|
||||||
|
scroll-padding-block: 1.5rem;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item,
|
||||||
|
.empty:not(:empty) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
outline: 0;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
&[data-highlighted] {
|
||||||
|
background-color: $blue-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemLabel {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $gray-400;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { Autocomplete as BaseAutocomplete } from '@base-ui/react/autocomplete';
|
||||||
|
import type { ReactNode, Ref } from 'react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
import type { InputProps } from '../input/input/Input';
|
||||||
|
import { getScrollParent } from './autocompleteInput.utils';
|
||||||
|
|
||||||
|
import inputStyles from '../input/input/Input.module.scss';
|
||||||
|
import styles from './AutocompleteInput.module.scss';
|
||||||
|
|
||||||
|
export interface AutocompleteInputProps extends Omit<InputProps, 'value' | 'defaultValue' | 'onChange'> {
|
||||||
|
options: string[];
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
emptyLabel?: string;
|
||||||
|
trailingElement?: (option: string) => ReactNode;
|
||||||
|
inputRef?: Ref<HTMLInputElement>;
|
||||||
|
openOnFocus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutocompleteInput({
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
emptyLabel,
|
||||||
|
fluid,
|
||||||
|
height = 'medium',
|
||||||
|
inputRef,
|
||||||
|
onValueChange,
|
||||||
|
options,
|
||||||
|
openOnFocus = false,
|
||||||
|
trailingElement,
|
||||||
|
value,
|
||||||
|
variant = 'subtle',
|
||||||
|
...inputProps
|
||||||
|
}: AutocompleteInputProps) {
|
||||||
|
const internalInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { onFocus, ...restInputProps } = inputProps;
|
||||||
|
|
||||||
|
const handleInputRef = (node: HTMLInputElement | null) => {
|
||||||
|
internalInputRef.current = node;
|
||||||
|
|
||||||
|
if (!inputRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof inputRef === 'function') {
|
||||||
|
inputRef(node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRef.current = node;
|
||||||
|
};
|
||||||
|
|
||||||
|
// close the popover when the parent scrollable container scrolls or the window resizes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollTarget = getScrollParent(internalInputRef.current);
|
||||||
|
const handleScroll = () => setOpen(false);
|
||||||
|
|
||||||
|
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
window.addEventListener('resize', handleScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollTarget.removeEventListener('scroll', handleScroll);
|
||||||
|
window.removeEventListener('resize', handleScroll);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseAutocomplete.Root
|
||||||
|
items={options}
|
||||||
|
autoHighlight
|
||||||
|
highlightItemOnHover
|
||||||
|
disabled={disabled}
|
||||||
|
open={open}
|
||||||
|
value={value}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onValueChange={onValueChange}
|
||||||
|
>
|
||||||
|
<BaseAutocomplete.Input
|
||||||
|
ref={handleInputRef}
|
||||||
|
onFocus={(event) => {
|
||||||
|
onFocus?.(event);
|
||||||
|
if (openOnFocus) {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cx([
|
||||||
|
inputStyles.input,
|
||||||
|
inputStyles[variant],
|
||||||
|
inputStyles[height],
|
||||||
|
fluid && inputStyles.fluid,
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
{...restInputProps}
|
||||||
|
/>
|
||||||
|
<BaseAutocomplete.Portal>
|
||||||
|
<BaseAutocomplete.Positioner side='bottom' align='start' className={styles.positioner}>
|
||||||
|
<BaseAutocomplete.Popup className={styles.popup} data-hide-empty={emptyLabel ? undefined : ''}>
|
||||||
|
<BaseAutocomplete.List className={styles.list}>
|
||||||
|
<BaseAutocomplete.Collection>
|
||||||
|
{(option: string) => (
|
||||||
|
<BaseAutocomplete.Item key={option} value={option} className={styles.item}>
|
||||||
|
<span className={styles.itemLabel}>{option}</span>
|
||||||
|
{trailingElement?.(option)}
|
||||||
|
</BaseAutocomplete.Item>
|
||||||
|
)}
|
||||||
|
</BaseAutocomplete.Collection>
|
||||||
|
{emptyLabel && <BaseAutocomplete.Empty className={styles.empty}>{emptyLabel}</BaseAutocomplete.Empty>}
|
||||||
|
</BaseAutocomplete.List>
|
||||||
|
</BaseAutocomplete.Popup>
|
||||||
|
</BaseAutocomplete.Positioner>
|
||||||
|
</BaseAutocomplete.Portal>
|
||||||
|
</BaseAutocomplete.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export function getScrollParent(node: HTMLElement | null): HTMLElement | Window {
|
||||||
|
let parent = node?.parentElement ?? null;
|
||||||
|
|
||||||
|
while (parent) {
|
||||||
|
const { overflowY } = window.getComputedStyle(parent);
|
||||||
|
if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
@use './buttonVariants' as base;
|
||||||
|
|
||||||
|
@include base.button-variants;
|
||||||
|
|
||||||
|
.baseButton {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
padding-inline: 1em;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $blue-500;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
cursor: wait;
|
||||||
|
.content {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingOverlay {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
stroke-dasharray: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
height: 2rem;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
height: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xlarge {
|
||||||
|
height: 3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||||
|
import { IoEllipseOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './Button.module.scss';
|
||||||
|
|
||||||
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?:
|
||||||
|
| 'primary'
|
||||||
|
| 'subtle'
|
||||||
|
| 'subtle-white'
|
||||||
|
| 'destructive'
|
||||||
|
| 'subtle-destructive'
|
||||||
|
| 'ghosted'
|
||||||
|
| 'ghosted-white'
|
||||||
|
| 'ghosted-destructive';
|
||||||
|
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||||
|
fluid?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, children, variant = 'subtle', size = 'medium', fluid, loading, ...buttonProps }, ref) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={cx([
|
||||||
|
style.baseButton,
|
||||||
|
style[variant],
|
||||||
|
style[size],
|
||||||
|
fluid && style.fluid,
|
||||||
|
loading && style.loading,
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
type='button'
|
||||||
|
disabled={loading || buttonProps.disabled}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
<span className={style.content}>{children}</span>
|
||||||
|
{loading && (
|
||||||
|
<div className={style.loadingOverlay}>
|
||||||
|
<IoEllipseOutline className={style.spinner} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@@ -1,26 +1,45 @@
|
|||||||
.subtle {
|
@use './buttonVariants' as base;
|
||||||
|
|
||||||
|
@include base.button-variants;
|
||||||
|
|
||||||
|
.baseIconButton {
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
height: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
|
|
||||||
background: $gray-1050;
|
|
||||||
color: $blue-400;
|
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 3px;
|
border-radius: $component-border-radius-md;
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
cursor: pointer;
|
||||||
background: $gray-1000;
|
|
||||||
color: $blue-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
&:focus-visible {
|
||||||
background: $gray-1100;
|
outline: 2px solid $blue-500;
|
||||||
border-color: $gray-1250;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background: $gray-1050;
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xlarge {
|
||||||
|
height: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,37 @@
|
|||||||
import { ButtonHTMLAttributes } from 'react';
|
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import style from './IconButton.module.scss';
|
import style from './IconButton.module.scss';
|
||||||
|
|
||||||
export default function IconButton(props: ButtonHTMLAttributes<HTMLButtonElement>) {
|
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
const { className, children, ...buttonProps } = props;
|
variant?:
|
||||||
return (
|
| 'primary'
|
||||||
<button className={cx([style.subtle, className])} {...buttonProps}>
|
| 'subtle'
|
||||||
{children}
|
| 'subtle-white'
|
||||||
</button>
|
| 'destructive'
|
||||||
);
|
| 'subtle-destructive'
|
||||||
|
| 'ghosted'
|
||||||
|
| 'ghosted-white'
|
||||||
|
| 'ghosted-destructive';
|
||||||
|
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||||
|
({ className, children, variant = 'subtle', size = 'medium', ...buttonProps }, ref) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={cx([style.baseIconButton, style[variant], style[size], className])}
|
||||||
|
type='button'
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
IconButton.displayName = 'IconButton';
|
||||||
|
|
||||||
|
export default IconButton;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { MouseEvent } from 'react';
|
|
||||||
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
interface TooltipActionBtnProps extends IconButtonProps {
|
|
||||||
clickHandler: (event: MouseEvent) => void | Promise<void>;
|
|
||||||
tooltip: string;
|
|
||||||
openDelay?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TooltipActionBtn(props: TooltipActionBtnProps) {
|
|
||||||
const { clickHandler, icon, size = 'xs', tooltip, openDelay = 0, className, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Tooltip label={tooltip} openDelay={openDelay}>
|
|
||||||
<IconButton {...rest} size={size} icon={icon} onClick={clickHandler} className={className} />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
@use '@/theme/themeTokens' as *;
|
||||||
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
@mixin button-variants {
|
||||||
|
.subtle {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $blue-400;
|
||||||
|
line-height: 1em;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-white {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: $gray-1050;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: $blue-700;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $blue-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $blue-800;
|
||||||
|
border-color: $blue-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $viewer-opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.destructive {
|
||||||
|
background: $red-700;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $red-600;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $red-800;
|
||||||
|
border-color: $red-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-destructive {
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $red-400;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted {
|
||||||
|
background: transparent;
|
||||||
|
color: $blue-500;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted-white {
|
||||||
|
background: transparent;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghosted-destructive {
|
||||||
|
background: transparent;
|
||||||
|
color: $red-500;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.checkbox {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: $component-border-radius-sm;
|
||||||
|
|
||||||
|
&[data-unchecked] {
|
||||||
|
background-color: $gray-1100;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $gray-1000;
|
||||||
|
}
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-checked] {
|
||||||
|
background-color: $blue-500;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $blue-400;
|
||||||
|
}
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $blue-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $blue-500;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
display: flex;
|
||||||
|
color: $ui-white;
|
||||||
|
|
||||||
|
&[data-unchecked] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
||||||
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import style from './Checkbox.module.scss';
|
||||||
|
|
||||||
|
export default function Checkbox(checkboxProps: BaseCheckbox.Root.Props) {
|
||||||
|
return (
|
||||||
|
<BaseCheckbox.Root className={style.checkbox} {...checkboxProps}>
|
||||||
|
<BaseCheckbox.Indicator className={style.indicator}>
|
||||||
|
<IoCheckmark className={style.icon} />
|
||||||
|
</BaseCheckbox.Indicator>
|
||||||
|
</BaseCheckbox.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import { IoArrowForward } from 'react-icons/io5';
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
Select,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { IoArrowForward } from '@react-icons/all-files/io5/IoArrowForward';
|
|
||||||
|
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
|
||||||
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||||
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
|
import Button from '../buttons/Button';
|
||||||
|
import Dialog from '../dialog/Dialog';
|
||||||
import Info from '../info/Info';
|
import Info from '../info/Info';
|
||||||
|
import Input from '../input/input/Input';
|
||||||
import AppLink from '../link/app-link/AppLink';
|
import AppLink from '../link/app-link/AppLink';
|
||||||
|
import Select from '../select/Select';
|
||||||
|
|
||||||
import style from './RedirectClientModal.module.scss';
|
import style from './RedirectClientModal.module.scss';
|
||||||
|
|
||||||
@@ -29,8 +22,7 @@ interface RedirectClientModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RedirectClientModal(props: RedirectClientModalProps) {
|
export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
|
||||||
const { id, isOpen, name, currentPath, origin, onClose } = props;
|
|
||||||
const { data } = useUrlPresets();
|
const { data } = useUrlPresets();
|
||||||
const [path, setPath] = useState(currentPath);
|
const [path, setPath] = useState(currentPath);
|
||||||
const [selected, setSelected] = useState('/');
|
const [selected, setSelected] = useState('/');
|
||||||
@@ -41,86 +33,91 @@ export function RedirectClientModal(props: RedirectClientModalProps) {
|
|||||||
if (newPath === '/' || newPath === currentPath) {
|
if (newPath === '/' || newPath === currentPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRedirect({ target: id, redirect: newPath });
|
|
||||||
|
if (newPath.startsWith('preset-')) {
|
||||||
|
setRedirect({ target: id, redirect: newPath.slice(7) });
|
||||||
|
} else {
|
||||||
|
setRedirect({ target: id, redirect: newPath });
|
||||||
|
}
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const enabledPresets = data.filter((preset) => preset.enabled);
|
const enabledPresets = data.filter((preset) => preset.enabled);
|
||||||
|
|
||||||
|
const viewOptions = [
|
||||||
|
...navigatorConstants.map((view) => ({
|
||||||
|
value: `/${view.url}`,
|
||||||
|
label: view.label,
|
||||||
|
})),
|
||||||
|
...enabledPresets.map((preset) => ({
|
||||||
|
value: `preset-${preset.alias}`,
|
||||||
|
label: `URL Preset: ${preset.alias}`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
<Dialog
|
||||||
<ModalOverlay />
|
isOpen={isOpen}
|
||||||
<ModalContent maxWidth='max(480px, 35vw)'>
|
onClose={onClose}
|
||||||
<ModalHeader>Redirect: {name}</ModalHeader>
|
showCloseButton
|
||||||
<ModalCloseButton />
|
showBackdrop
|
||||||
<ModalBody>
|
title={`Redirect: ${name}`}
|
||||||
|
bodyElements={
|
||||||
|
<>
|
||||||
<Info>
|
<Info>
|
||||||
Remotely redirect the client to a different URL. <br />
|
Remotely redirect the client to a different URL. <br />
|
||||||
Either by selecting a URL Preset or entering a custom path.
|
Either by entering a custom path or selecting a URL Preset.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<AppLink search='settings=feature_settings__urlpresets'>Manage URL Presets</AppLink>
|
<AppLink search='settings=sharing__presets'>Manage URL Presets</AppLink>
|
||||||
</Info>
|
</Info>
|
||||||
<div>
|
|
||||||
<span className={style.label}>Select View or URL Preset</span>
|
|
||||||
<div className={style.textEntry}>
|
|
||||||
<Select
|
|
||||||
size='md'
|
|
||||||
variant='ontime'
|
|
||||||
isDisabled={enabledPresets.length === 0}
|
|
||||||
onChange={(event) => setSelected(event.target.value)}
|
|
||||||
>
|
|
||||||
<option value='/'>Select view or preset</option>
|
|
||||||
{navigatorConstants.map((view) => {
|
|
||||||
return (
|
|
||||||
<option key={view.url} value={`/${view.url}`}>
|
|
||||||
{view.label}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{enabledPresets.map((preset) => {
|
|
||||||
return (
|
|
||||||
<option key={preset.pathAndParams} value={preset.pathAndParams}>
|
|
||||||
{`Preset: ${preset.alias}`}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
<IconButton
|
|
||||||
variant='ontime-filled'
|
|
||||||
size='md'
|
|
||||||
aria-label='Redirect to preset'
|
|
||||||
className={style.redirect}
|
|
||||||
icon={<IoArrowForward />}
|
|
||||||
isDisabled={enabledPresets.length === 0 || selected === '/'}
|
|
||||||
onClick={() => handleRedirect(selected)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.inlineEntry}>
|
<div className={style.inlineEntry}>
|
||||||
<span className={style.label}>Enter custom path</span>
|
<span className={style.label}>Enter custom path</span>
|
||||||
<label className={style.textEntry}>
|
<label className={style.textEntry}>
|
||||||
{origin}
|
{origin}
|
||||||
<Input
|
<Input placeholder='eg. /timer' fluid value={path} onChange={(event) => setPath(event.target.value)} />
|
||||||
variant='ontime-filled'
|
|
||||||
size='md'
|
|
||||||
placeholder='eg. /minimal?key=0000ffff'
|
|
||||||
value={path}
|
|
||||||
onChange={(event) => setPath(event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<IconButton
|
<Button
|
||||||
variant='ontime-filled'
|
variant='primary'
|
||||||
size='md'
|
|
||||||
aria-label='Redirect'
|
aria-label='Redirect'
|
||||||
isDisabled={path === currentPath || path === ''}
|
disabled={path === currentPath || path === ''}
|
||||||
className={style.redirect}
|
className={style.redirect}
|
||||||
icon={<IoArrowForward />}
|
|
||||||
onClick={() => handleRedirect(path)}
|
onClick={() => handleRedirect(path)}
|
||||||
/>
|
>
|
||||||
|
Redirect
|
||||||
|
<IoArrowForward />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
<div>
|
||||||
</ModalContent>
|
<span className={style.label}>Select View or URL Preset</span>
|
||||||
</Modal>
|
<div className={style.inlineEntry}>
|
||||||
|
<label className={style.textEntry}>
|
||||||
|
{origin}
|
||||||
|
<Select
|
||||||
|
fluid
|
||||||
|
options={viewOptions}
|
||||||
|
defaultValue={viewOptions[0].value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value === null) return;
|
||||||
|
setSelected(value);
|
||||||
|
}}
|
||||||
|
disabled={enabledPresets.length === 0}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
aria-label='Redirect to preset'
|
||||||
|
className={style.redirect}
|
||||||
|
disabled={enabledPresets.length === 0 || selected === '/'}
|
||||||
|
onClick={() => handleRedirect(selected)}
|
||||||
|
>
|
||||||
|
Redirect <IoArrowForward />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
|
import Button from '../../../common/components/buttons/Button';
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
|
import Dialog from '../dialog/Dialog';
|
||||||
|
import Input from '../input/input/Input';
|
||||||
|
|
||||||
interface RenameClientModalProps {
|
interface RenameClientModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -20,8 +12,7 @@ interface RenameClientModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenameClientModal(props: RenameClientModalProps) {
|
export function RenameClientModal({ id, name: currentName = '', isOpen, onClose }: RenameClientModalProps) {
|
||||||
const { id, name: currentName = '', isOpen, onClose } = props;
|
|
||||||
const [name, setName] = useState(currentName);
|
const [name, setName] = useState(currentName);
|
||||||
|
|
||||||
const { setClientName } = setClientRemote;
|
const { setClientName } = setClientRemote;
|
||||||
@@ -36,29 +27,24 @@ export function RenameClientModal(props: RenameClientModalProps) {
|
|||||||
const canSubmit = name !== currentName && name !== '';
|
const canSubmit = name !== currentName && name !== '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
<Dialog
|
||||||
<ModalOverlay />
|
isOpen={isOpen}
|
||||||
<ModalContent>
|
title={`Rename Client: ${currentName}`}
|
||||||
<ModalHeader>Rename: {currentName}</ModalHeader>
|
showCloseButton
|
||||||
<ModalCloseButton />
|
onClose={onClose}
|
||||||
<ModalBody>
|
bodyElements={
|
||||||
<Input
|
<Input height='large' placeholder='New name' value={name} onChange={(event) => setName(event.target.value)} />
|
||||||
variant='ontime-filled'
|
}
|
||||||
size='md'
|
footerElements={
|
||||||
placeholder='new name'
|
<>
|
||||||
value={name}
|
<Button variant='subtle' size='large' onClick={onClose}>
|
||||||
onChange={(event) => setName(event.target.value)}
|
|
||||||
/>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button size='md' variant='ontime-filled' onClick={handleRename} isDisabled={!canSubmit}>
|
<Button variant='primary' size='large' onClick={handleRename} disabled={!canSubmit}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</>
|
||||||
</ModalContent>
|
}
|
||||||
</Modal>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
.contextMenuButton {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contextMenuBackdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
// logic (with some modifications) culled from:
|
|
||||||
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
|
||||||
|
|
||||||
import { ReactElement } from 'react';
|
|
||||||
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
|
|
||||||
import { IconType } from '@react-icons/all-files';
|
|
||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
import { ContextMenuOption } from './ContextMenuOption';
|
|
||||||
|
|
||||||
import style from './ContextMenu.module.scss';
|
|
||||||
|
|
||||||
type ContextMenuCoords = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OptionWithoutGroup = {
|
|
||||||
label: string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
icon: IconType;
|
|
||||||
onClick: () => void;
|
|
||||||
withDivider?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OptionWithGroup = {
|
|
||||||
label: string;
|
|
||||||
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Option = OptionWithoutGroup | OptionWithGroup;
|
|
||||||
|
|
||||||
const isOptionWithGroup = (option: Option): option is OptionWithGroup => 'group' in option;
|
|
||||||
|
|
||||||
type ContextMenuStore = {
|
|
||||||
coords: ContextMenuCoords;
|
|
||||||
options: Option[];
|
|
||||||
isOpen: boolean;
|
|
||||||
setContextMenu: (coords: ContextMenuCoords, options: Option[]) => void;
|
|
||||||
setIsOpen: (newIsOpen: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useContextMenuStore = create<ContextMenuStore>((set) => ({
|
|
||||||
coords: { x: 0, y: 0 },
|
|
||||||
options: [],
|
|
||||||
isOpen: false,
|
|
||||||
setContextMenu: (coords, options) => set(() => ({ coords, options, isOpen: true })),
|
|
||||||
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface ContextMenuProps {
|
|
||||||
// ReactElement type required due to early `return` (line 51) returning {children}
|
|
||||||
children: ReactElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContextMenu = ({ children }: ContextMenuProps) => {
|
|
||||||
const { coords, options, isOpen, setIsOpen } = useContextMenuStore();
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
return setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<div className={style.contextMenuBackdrop} />
|
|
||||||
<Menu isOpen size='sm' gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
|
|
||||||
<MenuButton
|
|
||||||
className={style.contextMenuButton}
|
|
||||||
aria-hidden
|
|
||||||
w={1}
|
|
||||||
h={1}
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
left: coords.x,
|
|
||||||
top: coords.y,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuList>
|
|
||||||
{options.map((option) =>
|
|
||||||
isOptionWithGroup(option) ? (
|
|
||||||
<MenuGroup key={option.label} title={option.label}>
|
|
||||||
{option.group.map((groupOption) => (
|
|
||||||
<ContextMenuOption key={groupOption.label} {...groupOption} />
|
|
||||||
))}
|
|
||||||
</MenuGroup>
|
|
||||||
) : (
|
|
||||||
<ContextMenuOption key={option.label} {...option} />
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { MenuDivider, MenuItem } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { OptionWithoutGroup } from './ContextMenu';
|
|
||||||
|
|
||||||
export const ContextMenuOption = ({ label, onClick, isDisabled, icon: Icon, withDivider }: OptionWithoutGroup) => (
|
|
||||||
<>
|
|
||||||
{withDivider && <MenuDivider />}
|
|
||||||
<MenuItem icon={<Icon style={{ fontSize: '1rem' }} />} onClick={onClick} isDisabled={isDisabled}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
.copytag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
border-radius: 3px 0 0 3px;
|
||||||
|
border: 1px solid $gray-1200;
|
||||||
|
background-color: $white-3;
|
||||||
|
padding-inline: 0.5rem 1rem;
|
||||||
|
line-height: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
height: 2rem;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
height: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
@@ -1,47 +1,57 @@
|
|||||||
import { PropsWithChildren, useState } from 'react';
|
import { PropsWithChildren, useRef, useState } from 'react';
|
||||||
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
import { IoCheckmark, IoCopy } from 'react-icons/io5';
|
||||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
|
||||||
import { IoCopy } from '@react-icons/all-files/io5/IoCopy';
|
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
import { copyToClipboard } from '../../utils/copyToClipboard';
|
||||||
import { Size } from '../../models/Util.type';
|
import { cx } from '../../utils/styleUtils';
|
||||||
import copyToClipboard from '../../utils/copyToClipboard';
|
import Button from '../buttons/Button';
|
||||||
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
|
import style from './CopyTag.module.scss';
|
||||||
|
|
||||||
interface CopyTagProps {
|
interface CopyTagProps {
|
||||||
copyValue: string;
|
copyValue: string;
|
||||||
label: string;
|
|
||||||
size?: Size;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
size?: 'small' | 'medium' | 'large';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
|
export default function CopyTag({
|
||||||
const { copyValue, label, size = 'xs', disabled, children, onClick } = props;
|
copyValue,
|
||||||
|
disabled,
|
||||||
|
size = 'medium',
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
}: PropsWithChildren<CopyTagProps>) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = async () => {
|
||||||
copyToClipboard(copyValue);
|
try {
|
||||||
setCopied(true);
|
await copyToClipboard(copyValue);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
// reset copied state
|
// reset copied state
|
||||||
setTimeout(() => setCopied(false), 1000);
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label} openDelay={tooltipDelayFast}>
|
<div className={style.copytag}>
|
||||||
<ButtonGroup size={size} isAttached>
|
{onClick !== undefined ? (
|
||||||
<Button variant='ontime-subtle' tabIndex={-1} onClick={onClick} isDisabled={disabled}>
|
<Button className={style.action} size={size} onClick={onClick} disabled={disabled}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton
|
) : (
|
||||||
aria-label={label}
|
<div className={cx([style.label, style[size]])}>{children}</div>
|
||||||
icon={copied ? <IoCheckmark /> : <IoCopy />}
|
)}
|
||||||
variant='ontime-filled'
|
<IconButton className={style.copy} variant='primary' size={size} onClick={handleClick} disabled={disabled}>
|
||||||
tabIndex={-1}
|
{copied ? <IoCheckmark /> : <IoCopy />}
|
||||||
onClick={handleClick}
|
</IconButton>
|
||||||
isDisabled={disabled}
|
</div>
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.delaySymbol {
|
.delaySymbol {
|
||||||
svg {
|
svg {
|
||||||
|
display: inline;
|
||||||
|
vertical-align: middle;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: $ontime-delay;
|
color: $ontime-delay;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Tooltip } from '@chakra-ui/react';
|
import { IoChevronDown, IoChevronUp } from 'react-icons/io5';
|
||||||
import { IoChevronDown } from '@react-icons/all-files/io5/IoChevronDown';
|
|
||||||
import { IoChevronUp } from '@react-icons/all-files/io5/IoChevronUp';
|
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
|
||||||
import { millisToDelayString } from '../../utils/dateConfig';
|
import { millisToDelayString } from '../../utils/dateConfig';
|
||||||
|
import Tooltip from '../tooltip/Tooltip';
|
||||||
|
|
||||||
import style from './DelayIndicator.module.scss';
|
import style from './DelayIndicator.module.scss';
|
||||||
|
|
||||||
@@ -24,8 +22,8 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
|
|||||||
: millisToDelayString(delayValue);
|
: millisToDelayString(delayValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip openDelay={tooltipDelayFast} label={delayString}>
|
<Tooltip text={delayString} render={<span />} className={style.delaySymbol}>
|
||||||
<span className={style.delaySymbol}>{delayValue < 0 ? <IoChevronDown /> : <IoChevronUp />}</span>
|
{delayValue < 0 ? <IoChevronDown /> : <IoChevronUp />}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
.dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 10%;
|
||||||
|
left: 50%;
|
||||||
|
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
padding-inline: 1rem;
|
||||||
|
min-width: min(600px, 90vw);
|
||||||
|
|
||||||
|
background-color: $gray-1250;
|
||||||
|
color: $ui-white;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: $box-shadow-l1;
|
||||||
|
border: 1px solid $gray-1100;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: $backdrop-color;
|
||||||
|
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||||
|
|
||||||
|
&[data-starting-style],
|
||||||
|
&[data-ending-style] {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-block: 1rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
max-height: min(80vh, 600px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding-block: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { IoClose } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
|
import style from './Dialog.module.scss';
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
showBackdrop?: boolean;
|
||||||
|
bodyElements: ReactNode;
|
||||||
|
footerElements?: ReactNode;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Dialog({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
showCloseButton,
|
||||||
|
showBackdrop,
|
||||||
|
bodyElements,
|
||||||
|
footerElements,
|
||||||
|
onClose,
|
||||||
|
}: DialogProps) {
|
||||||
|
return (
|
||||||
|
<BaseDialog.Root
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BaseDialog.Portal>
|
||||||
|
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
||||||
|
<BaseDialog.Popup className={style.dialog}>
|
||||||
|
<div className={style.title}>
|
||||||
|
{title}
|
||||||
|
{showCloseButton && (
|
||||||
|
<IconButton variant='subtle-white' onClick={onClose}>
|
||||||
|
<IoClose />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={style.body}>{bodyElements}</div>
|
||||||
|
<div className={style.footer}>{footerElements}</div>
|
||||||
|
</BaseDialog.Popup>
|
||||||
|
</BaseDialog.Portal>
|
||||||
|
</BaseDialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
.positioner {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-block: 0.25rem;
|
||||||
|
|
||||||
|
color: $ui-white;
|
||||||
|
background-color: $gray-1250;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid $gray-1000;
|
||||||
|
box-shadow: $box-shadow-l1;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
transform-origin: var(--transform-origin);
|
||||||
|
transition:
|
||||||
|
transform 150ms,
|
||||||
|
opacity 150ms;
|
||||||
|
|
||||||
|
&[data-starting-style],
|
||||||
|
&[data-ending-style] {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
outline: 0;
|
||||||
|
cursor: default;
|
||||||
|
padding-block: 0.5rem;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: $gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-highlighted] {
|
||||||
|
z-index: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-highlighted]:not([data-disabled])::before {
|
||||||
|
content: '';
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
inset-block: 0;
|
||||||
|
inset-inline: 0.25rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: $gray-1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type='destructive'] {
|
||||||
|
color: $red-500;
|
||||||
|
svg {
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item .iconStrong {
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: $gray-400;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
color: $gray-500;
|
||||||
|
font-size: calc(1rem - 4px);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
margin: 0.25rem 0.75rem;
|
||||||
|
height: 1px;
|
||||||
|
background-color: $white-7;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { IconType } from 'react-icons';
|
||||||
|
|
||||||
|
import style from './DropdownMenu.module.scss';
|
||||||
|
|
||||||
|
type DropdownMenuItemDivider = { type: 'divider' };
|
||||||
|
type DropdownMenuItem = {
|
||||||
|
type: 'item' | 'destructive';
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: IconType;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
shortcut?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DropdownMenuOption = DropdownMenuItemDivider | DropdownMenuItem;
|
||||||
|
|
||||||
|
interface DropdownMenuProps extends BaseMenu.Trigger.Props {
|
||||||
|
items: DropdownMenuOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChildren<DropdownMenuProps>) {
|
||||||
|
return (
|
||||||
|
<BaseMenu.Root>
|
||||||
|
<BaseMenu.Trigger {...triggerProps}>{children}</BaseMenu.Trigger>
|
||||||
|
<BaseMenu.Portal>
|
||||||
|
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8}>
|
||||||
|
<BaseMenu.Popup className={style.popup}>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
if (item.type === 'divider') {
|
||||||
|
return <BaseMenu.Separator key={index} className={style.separator} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BaseMenu.Item
|
||||||
|
key={index}
|
||||||
|
className={style.item}
|
||||||
|
onClick={item.onClick}
|
||||||
|
disabled={item.disabled}
|
||||||
|
data-type={item.type}
|
||||||
|
>
|
||||||
|
<span className={style.content}>
|
||||||
|
<span className={style.label}>
|
||||||
|
{item.icon && <item.icon />}
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
{item.description && <span className={style.description}>{item.description}</span>}
|
||||||
|
</span>
|
||||||
|
{item.shortcut && <span className={style.shortcut}>{item.shortcut}</span>}
|
||||||
|
</BaseMenu.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BaseMenu.Popup>
|
||||||
|
</BaseMenu.Positioner>
|
||||||
|
</BaseMenu.Portal>
|
||||||
|
</BaseMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PositionedDropdownMenuProps {
|
||||||
|
items: Array<DropdownMenuItemDivider | DropdownMenuItem>;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PositionedDropdownMenu({ items, isOpen, position, onClose }: PositionedDropdownMenuProps) {
|
||||||
|
return (
|
||||||
|
<BaseMenu.Root
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BaseMenu.Trigger style={{ position: 'fixed', left: position.x, top: position.y }} aria-hidden />
|
||||||
|
<BaseMenu.Portal>
|
||||||
|
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8} alignOffset={8}>
|
||||||
|
<BaseMenu.Popup className={style.popup}>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
if (item.type === 'divider') {
|
||||||
|
return <BaseMenu.Separator key={index} className={style.separator} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||||
|
<span className={style.content}>
|
||||||
|
<span className={style.label}>
|
||||||
|
{item.icon && <item.icon />}
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
{item.description && <span className={style.description}>{item.description}</span>}
|
||||||
|
</span>
|
||||||
|
{item.shortcut && <span className={style.shortcut}>{item.shortcut}</span>}
|
||||||
|
</BaseMenu.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BaseMenu.Popup>
|
||||||
|
</BaseMenu.Positioner>
|
||||||
|
</BaseMenu.Portal>
|
||||||
|
</BaseMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
.corner {
|
.arrow {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.corner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
@@ -21,6 +22,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.offsetCorner {
|
||||||
|
right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -40,3 +45,24 @@
|
|||||||
margin-bottom: $element-inner-spacing;
|
margin-bottom: $element-inner-spacing;
|
||||||
max-width: max-content; // prevent label from taking entire row
|
max-width: max-content; // prevent label from taking entire row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
background-color: $border-color-ondark;
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
width: 1px;
|
||||||
|
height: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
border-radius: $panel-border-radius;
|
||||||
|
background-color: $bg-container-l2;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { HTMLAttributes, JSX, LabelHTMLAttributes, MouseEventHandler } from 'react';
|
||||||
|
import { IconBaseProps } from 'react-icons';
|
||||||
|
import { IoArrowUp } from 'react-icons/io5';
|
||||||
|
import { TbPictureInPictureOff } from 'react-icons/tb';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './EditorUtils.module.scss';
|
||||||
|
|
||||||
|
export function CornerExtract({ className, ...elementProps }: IconBaseProps) {
|
||||||
|
return <IoArrowUp className={cx([style.corner, style.arrow, className])} {...elementProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CornerPipButton({ className, ...elementProps }: IconBaseProps) {
|
||||||
|
return <TbPictureInPictureOff className={cx([style.corner, style.offsetCorner, className])} {...elementProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExtractAndPip extends IconBaseProps {
|
||||||
|
onExtractClick: MouseEventHandler<SVGElement>;
|
||||||
|
pipElement: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CornerWithPip({ className, pipElement, onExtractClick }: ExtractAndPip) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IoArrowUp className={cx([style.corner, style.arrow, className])} onClick={onExtractClick} />
|
||||||
|
{/* the pip element returns the icon button */}
|
||||||
|
{pipElement}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Title({ children, className, ...elementProps }: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
const classes = cx([style.title, className]);
|
||||||
|
return (
|
||||||
|
<h3 className={classes} {...elementProps}>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Label({ children, className, ...elementProps }: LabelHTMLAttributes<HTMLLabelElement>) {
|
||||||
|
const classes = cx([style.label, className]);
|
||||||
|
return (
|
||||||
|
<label className={classes} {...elementProps}>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
orientation?: 'horizontal' | 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) {
|
||||||
|
return <div className={cx([style.separator, style[orientation], className])} role='separator' {...elementProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Panel({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cx([style.panel, className])} {...props} />;
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
/* eslint-disable react/destructuring-assignment */
|
|
||||||
import React from 'react';
|
|
||||||
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { runtimeStore } from '@/common/stores/runtime';
|
import { runtimeStore } from '../../stores/runtime';
|
||||||
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
import { getConnectionState, getReconnectAttempts } from '../../utils/socket';
|
||||||
|
|
||||||
import style from './ErrorBoundary.module.scss';
|
import style from './ErrorBoundary.module.scss';
|
||||||
|
|
||||||
@@ -26,17 +25,22 @@ class ErrorBoundary extends React.Component {
|
|||||||
error,
|
error,
|
||||||
errorInfo: info,
|
errorInfo: info,
|
||||||
});
|
});
|
||||||
|
this.reportContent = `${error} ${info.componentStack}`;
|
||||||
|
|
||||||
|
const appState = runtimeStore.getState();
|
||||||
|
if (appState.ping < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Sentry.withScope((scope) => {
|
Sentry.withScope((scope) => {
|
||||||
scope.setExtras({
|
scope.setExtras({
|
||||||
error,
|
error,
|
||||||
store: runtimeStore.getState(),
|
store: appState,
|
||||||
hasSocket: { hasConnected, shouldReconnect, reconnectAttempts },
|
hasSocket: { hasConnected: getConnectionState(), reconnectAttempts: getReconnectAttempts() },
|
||||||
});
|
});
|
||||||
const eventId = Sentry.captureException(error);
|
const eventId = Sentry.captureException(error);
|
||||||
this.setState({ eventId, info });
|
this.setState({ eventId, info });
|
||||||
});
|
});
|
||||||
this.reportContent = `${error} ${info.componentStack}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -44,15 +48,14 @@ class ErrorBoundary extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className={style.errorContainer} data-testid='error-container'>
|
<div className={style.errorContainer} data-testid='error-container'>
|
||||||
<div>
|
<div>
|
||||||
<p className={style.error}>:/</p>
|
<p className={style.error}>: /</p>
|
||||||
<p>Something went wrong</p>
|
<p>Something went wrong.</p>
|
||||||
<div
|
<a
|
||||||
role='button'
|
|
||||||
className={style.report}
|
className={style.report}
|
||||||
onClick={() => Sentry.showReportDialog({ eventId: this.state.eventId })}
|
href={`mailto:mail@getontime.no?subject=Error%20Report&body=${encodeURIComponent(this.reportContent)}`}
|
||||||
>
|
>
|
||||||
Report error
|
Report error
|
||||||
</div>
|
</a>
|
||||||
<div
|
<div
|
||||||
role='button'
|
role='button'
|
||||||
className={style.report}
|
className={style.report}
|
||||||
|
|||||||
@@ -3,24 +3,21 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
background-color: #121212;
|
background-color: $ui-black;
|
||||||
color: white;
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: $error-red;
|
color: $error-red;
|
||||||
font-weight: 600;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.report {
|
.report {
|
||||||
text-decoration: underline $error-red;
|
color: $blue-500;
|
||||||
cursor: pointer;
|
text-decoration: underline;
|
||||||
}
|
text-underline-offset: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.report:hover {
|
&:hover {
|
||||||
color: $error-red;
|
color: $ontime-color;
|
||||||
}
|
|
||||||
|
|
||||||
.report:active {
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,13 @@ interface FitTextProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
max?: number; // inclusive
|
max?: number; // inclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FitText(props: PropsWithChildren<FitTextProps>) {
|
export function FitText({
|
||||||
const { children, mode = 'multi', min = 16, max = 256, ...elementProps } = props;
|
children,
|
||||||
|
mode = 'multi',
|
||||||
|
min = 16,
|
||||||
|
max = 256,
|
||||||
|
...elementProps
|
||||||
|
}: PropsWithChildren<FitTextProps>) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const isOverflown = useCallback(() => {
|
const isOverflown = useCallback(() => {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IconBaseProps } from 'react-icons';
|
||||||
|
import { IoLink } from 'react-icons/io5';
|
||||||
|
|
||||||
|
export default function RotatedLink(linkProps: IconBaseProps) {
|
||||||
|
return <IoLink style={{ transform: 'rotate(-45deg)' }} {...linkProps} />;
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 100;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
import { useClientStore } from '../../stores/clientStore';
|
import { useClientStore } from '../../stores/clientStore';
|
||||||
|
|||||||
@@ -7,15 +7,56 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: $text-body-size;
|
font-size: $text-body-size;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
color: $gray-200;
|
||||||
.content {
|
|
||||||
color: $gray-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
min-width: 1.5rem;
|
min-width: 1.5rem;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title,
|
||||||
|
.body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $ui-white;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
color: $gray-400;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
svg {
|
||||||
color: $info-blue;
|
color: $info-blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
svg {
|
||||||
|
color: $orange-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
svg {
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { IoAlertCircle } from '@react-icons/all-files/io5/IoAlertCircle';
|
import { IoAlertCircle, IoWarning } from 'react-icons/io5';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
@@ -7,15 +7,40 @@ import style from './Info.module.scss';
|
|||||||
|
|
||||||
interface InfoProps {
|
interface InfoProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
type?: 'info' | 'warning' | 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Info(props: PropsWithChildren<InfoProps>) {
|
interface InfoSectionProps {
|
||||||
const { className, children } = props;
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoTitle({ className, children }: PropsWithChildren<InfoSectionProps>) {
|
||||||
|
return <p className={cx([style.title, className])}>{children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoBody({ className, children }: PropsWithChildren<InfoSectionProps>) {
|
||||||
|
return <p className={cx([style.body, className])}>{children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoFooter({ className, children }: PropsWithChildren<InfoSectionProps>) {
|
||||||
|
return <div className={cx([style.footer, className])}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoRoot({ className, type = 'info', children }: PropsWithChildren<InfoProps>) {
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.infoLabel, className])}>
|
<div className={cx([style.infoLabel, style[type], className])}>
|
||||||
<IoAlertCircle />
|
{type === 'info' && <IoAlertCircle />}
|
||||||
<div>{children}</div>
|
{type === 'warning' && <IoWarning />}
|
||||||
|
{type === 'error' && <IoWarning />}
|
||||||
|
<div className={style.content}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Info = Object.assign(InfoRoot, {
|
||||||
|
Title: InfoTitle,
|
||||||
|
Body: InfoBody,
|
||||||
|
Footer: InfoFooter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Info;
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { RefObject, useEffect } from 'react';
|
|
||||||
import { Textarea, TextareaProps } from '@chakra-ui/react';
|
|
||||||
// @ts-expect-error no types from library
|
|
||||||
import autosize from 'autosize/dist/autosize';
|
|
||||||
|
|
||||||
export const AutoTextArea = (props: TextareaProps & { inputref: RefObject<unknown> }) => {
|
|
||||||
const { value, inputref } = props;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const node = inputref.current;
|
|
||||||
autosize(inputref.current);
|
|
||||||
return () => {
|
|
||||||
autosize.destroy(node);
|
|
||||||
};
|
|
||||||
}, [inputref, value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
overflow='hidden'
|
|
||||||
w='100%'
|
|
||||||
ref={inputref}
|
|
||||||
resize='none'
|
|
||||||
transition='height none'
|
|
||||||
variant='ontime-transparent'
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// @ts-expect-error no types from library
|
||||||
|
import autosize from 'autosize/dist/autosize';
|
||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
|
||||||
|
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
||||||
|
|
||||||
|
interface AutoTextAreaProps extends TextareaProps {
|
||||||
|
inputref: RefObject<HTMLTextAreaElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A textarea that automatically resizes based on its content
|
||||||
|
*/
|
||||||
|
export function AutoTextarea({ value, inputref, ...textAreaProps }: AutoTextAreaProps) {
|
||||||
|
// when the value changes, we use the ref to reapply autosize
|
||||||
|
useEffect(() => {
|
||||||
|
const node = inputref.current;
|
||||||
|
autosize(inputref.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
autosize.destroy(node);
|
||||||
|
};
|
||||||
|
}, [inputref, value]);
|
||||||
|
|
||||||
|
return <Textarea ref={inputref} value={value} {...textAreaProps} />;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
import { MouseEvent } from 'react';
|
||||||
|
import { IoBan } from 'react-icons/io5';
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
@@ -10,12 +11,13 @@ interface SwatchProps {
|
|||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Swatch(props: SwatchProps) {
|
export default function Swatch({ color, isSelected, onClick }: SwatchProps) {
|
||||||
const { color, isSelected, onClick } = props;
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
onClick?.(color);
|
onClick?.(color);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
const classes = cx([style.swatch, isSelected && style.selected, onClick && style.selectable]);
|
||||||
|
|
||||||
if (!color) {
|
if (!color) {
|
||||||
|
|||||||