diff --git a/.env b/.env index ea5f638f..69a1c147 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ -DATABASE_URL=postgres://postgres@localhost:5432/jirs +DEBUG=true +NODE_ENV=development RUST_LOG=debug JIRS_CLIENT_PORT=7000 JIRS_CLIENT_BIND=0.0.0.0 +DATABASE_URL=postgres://postgres@localhost:5432/jirs JIRS_SERVER_PORT=5000 JIRS_SERVER_BIND=0.0.0.0 -NODE_ENV=development -DEBUG=true diff --git a/.gitignore b/.gitignore index ea8c4bf7..8c6cbac2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +mail.toml +web.toml +db.toml diff --git a/Cargo.lock b/Cargo.lock index e0503ca4..929ff717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ dependencies = [ "actix-service", "actix-threadpool", "actix-utils", - "base64", + "base64 0.11.0", "bitflags", "brotli2", "bytes", @@ -359,6 +359,12 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d663a8e9a99154b5fb793032533f6328da35e23aac63d5c152279aa8ba356825" +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + [[package]] name = "async-trait" version = "0.1.26" @@ -381,6 +387,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + [[package]] name = "autocfg" version = "1.0.0" @@ -397,7 +409,7 @@ dependencies = [ "actix-http", "actix-rt", "actix-service", - "base64", + "base64 0.11.0", "bytes", "derive_more", "futures-core", @@ -432,6 +444,25 @@ dependencies = [ "libc", ] +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + [[package]] name = "base64" version = "0.11.0" @@ -506,6 +537,12 @@ dependencies = [ "libc", ] +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + [[package]] name = "bumpalo" version = "3.2.1" @@ -630,6 +667,22 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ff9c56c9fb2a49c05ef0e431485a22400af20d33226dc0764d891d09e724127" +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "crc32fast" version = "1.2.0" @@ -655,7 +708,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "autocfg", + "autocfg 1.0.0", "cfg-if", "lazy_static", ] @@ -742,12 +795,91 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +[[package]] +name = "email" +version = "0.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91549a51bb0241165f13d57fc4c72cef063b4088fb078b019ecbf464a45f22e4" +dependencies = [ + "base64 0.9.3", + "chrono", + "encoding", + "lazy_static", + "rand 0.4.6", + "time", + "version_check 0.1.5", +] + [[package]] name = "enclose" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1056f553da426e9c025a662efa48b52e62e0a3a7648aa2d15aeaaf7f0d329357" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.22" @@ -826,6 +958,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + [[package]] name = "flate2" version = "1.0.14" @@ -844,6 +985,21 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1049,6 +1205,16 @@ dependencies = [ "libc", ] +[[package]] +name = "hostname" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" +dependencies = [ + "libc", + "winutil", +] + [[package]] name = "hostname" version = "0.3.1" @@ -1114,7 +1280,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" dependencies = [ - "autocfg", + "autocfg 1.0.0", ] [[package]] @@ -1207,6 +1373,8 @@ dependencies = [ "futures 0.3.4", "ipnetwork", "jirs-data", + "lettre", + "lettre_email", "libc", "log 0.4.8", "num-bigint", @@ -1220,6 +1388,7 @@ dependencies = [ "serde", "serde_json", "time", + "toml", "url 2.1.1", "uuid 0.8.1", ] @@ -1255,6 +1424,38 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lettre" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66afaa5dfadbb81d4e00fd1d1ab057c7cd4c799c5a44e0009386d553587e728" +dependencies = [ + "base64 0.10.1", + "bufstream", + "fast_chemail", + "hostname 0.1.5", + "log 0.4.8", + "native-tls", + "nom", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "lettre_email" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb68ca999042d965476e47bbdbacd52db0927348b6f8062c44dd04a3b1fd43b" +dependencies = [ + "base64 0.10.1", + "email", + "lettre", + "mime", + "time", + "uuid 0.7.4", +] + [[package]] name = "libc" version = "0.2.68" @@ -1399,6 +1600,24 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "native-tls" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" +dependencies = [ + "lazy_static", + "libc", + "log 0.4.8", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "net2" version = "0.2.33" @@ -1410,13 +1629,23 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr 2.3.3", + "version_check 0.1.5", +] + [[package]] name = "num-bigint" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" dependencies = [ - "autocfg", + "autocfg 1.0.0", "num-integer", "num-traits", ] @@ -1427,7 +1656,7 @@ version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" dependencies = [ - "autocfg", + "autocfg 1.0.0", "num-traits", ] @@ -1437,7 +1666,7 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" dependencies = [ - "autocfg", + "autocfg 1.0.0", ] [[package]] @@ -1456,6 +1685,39 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +[[package]] +name = "openssl" +version = "0.10.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7717097d810a0f2e2323f9e5d11e71608355e24828410b55b9d4f18aa5f9a5d8" +dependencies = [ + "autocfg 1.0.0", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.10.0" @@ -1567,6 +1829,12 @@ version = "0.1.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" +[[package]] +name = "pkg-config" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" + [[package]] name = "ppv-lite86" version = "0.2.6" @@ -1685,6 +1953,25 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.8", +] + [[package]] name = "rand" version = "0.7.3" @@ -1693,9 +1980,19 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom", "libc", - "rand_chacha", + "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", ] [[package]] @@ -1732,6 +2029,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -1741,6 +2047,59 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.8", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.8", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -1793,13 +2152,22 @@ version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" +[[package]] +name = "remove_dir_all" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "resolv-conf" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a" dependencies = [ - "hostname", + "hostname 0.3.1", "quick-error", ] @@ -1815,6 +2183,22 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "schannel" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039c25b130bd8c1321ee2d7de7fde2659fa9c2744e4bb29711cfc852ea53cd19" +dependencies = [ + "lazy_static", + "winapi 0.3.8", +] + [[package]] name = "scheduled-thread-pool" version = "0.2.4" @@ -1830,6 +2214,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "security-framework" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572dfa3a0785509e7a44b5b4bebcf94d41ba34e9ed9eb9df722545c3b3c4144a" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb15a5fec93b7021b8a9e96009c5d8d51c15673569f7c0f6b7204e5b7b404f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "seed" version = "0.6.0" @@ -1977,6 +2384,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand 0.7.3", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.8", +] + [[package]] name = "termcolor" version = "1.1.0" @@ -2078,6 +2499,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + [[package]] name = "trust-dns-proto" version = "0.18.0-alpha.2" @@ -2239,6 +2669,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "uuid" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" +dependencies = [ + "rand 0.6.5", +] + [[package]] name = "uuid" version = "0.8.1" @@ -2416,6 +2855,15 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "winutil" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 4244e7d0..a9c81653 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -23,90 +23,85 @@ pub type AvatarFilterActive = bool; pub type AppType = App>; #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] -pub enum EditIssueModalFieldId { - IssueType, - Title, - Description, - Status, - Assignees, - Reporter, - Priority, - Estimate, - TimeSpend, - TimeRemaining, - // comment - CommentBody, -} - -#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] -pub enum AddIssueModalFieldId { - IssueType, - Summary, - Description, - Reporter, - Assignees, - Priority, -} - -#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] -pub enum ProjectSettingsFieldId { - Name, - Url, - Description, - Category, -} - -#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] -pub enum LoginFieldId { - Username, - Email, - Token, +pub enum EditIssueModalSection { + Issue(IssueFieldId), + Comment(CommentFieldId), } #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] pub enum FieldId { Login(LoginFieldId), // issue - AddIssueModal(AddIssueModalFieldId), - EditIssueModal(EditIssueModalFieldId), + AddIssueModal(IssueFieldId), + EditIssueModal(EditIssueModalSection), // project boards TextFilterBoard, CopyButtonLabel, - ProjectSettings(ProjectSettingsFieldId), + ProjectSettings(ProjectFieldId), } impl std::fmt::Display for FieldId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { FieldId::EditIssueModal(sub) => match sub { - EditIssueModalFieldId::IssueType => f.write_str("issueTypeEditModalTop"), - EditIssueModalFieldId::Title => f.write_str("titleIssueEditModal"), - EditIssueModalFieldId::Description => f.write_str("descriptionIssueEditModal"), - EditIssueModalFieldId::Status => f.write_str("statusIssueEditModal"), - EditIssueModalFieldId::Assignees => f.write_str("assigneesIssueEditModal"), - EditIssueModalFieldId::Reporter => f.write_str("reporterIssueEditModal"), - EditIssueModalFieldId::Priority => f.write_str("priorityIssueEditModal"), - EditIssueModalFieldId::Estimate => f.write_str("estimateIssueEditModal"), - EditIssueModalFieldId::TimeSpend => f.write_str("timeSpendIssueEditModal"), - EditIssueModalFieldId::TimeRemaining => f.write_str("timeRemainingIssueEditModal"), - EditIssueModalFieldId::CommentBody => f.write_str("editIssue-commentBody"), + EditIssueModalSection::Issue(IssueFieldId::Type) => { + f.write_str("issueTypeEditModalTop") + } + EditIssueModalSection::Issue(IssueFieldId::Title) => { + f.write_str("titleIssueEditModal") + } + EditIssueModalSection::Issue(IssueFieldId::Description) => { + f.write_str("descriptionIssueEditModal") + } + EditIssueModalSection::Issue(IssueFieldId::Status) => { + f.write_str("statusIssueEditModal") + } + EditIssueModalSection::Issue(IssueFieldId::Assignees) => { + f.write_str("assigneesIssueEditModal") + } + EditIssueModalSection::Issue(IssueFieldId::Reporter) => { + f.write_str("reporterIssueEditModal") + } + EditIssueModalSection::Issue(IssueFieldId::Priority) => { + f.write_str("priorityIssueEditModal") + } + EditIssueModalSection::Issue(IssueFieldId::Estimate) => { + f.write_str("estimateIssueEditModal") + } + EditIssueModalSection::Issue(IssueFieldId::TimeSpend) => { + f.write_str("timeSpendIssueEditModal") + } + EditIssueModalSection::Issue(IssueFieldId::TimeRemaining) => { + f.write_str("timeRemainingIssueEditModal") + } + EditIssueModalSection::Comment(CommentFieldId::Body) => { + f.write_str("editIssue-commentBody") + } + EditIssueModalSection::Issue(IssueFieldId::ListPosition) => { + f.write_str("editIssue-listPosition") + } }, FieldId::AddIssueModal(sub) => match sub { - AddIssueModalFieldId::IssueType => f.write_str("issueTypeAddIssueModal"), - AddIssueModalFieldId::Summary => f.write_str("summaryAddIssueModal"), - AddIssueModalFieldId::Description => f.write_str("descriptionAddIssueModal"), - AddIssueModalFieldId::Reporter => f.write_str("reporterAddIssueModal"), - AddIssueModalFieldId::Assignees => f.write_str("assigneesAddIssueModal"), - AddIssueModalFieldId::Priority => f.write_str("issuePriorityAddIssueModal"), + IssueFieldId::Type => f.write_str("issueTypeAddIssueModal"), + IssueFieldId::Title => f.write_str("summaryAddIssueModal"), + IssueFieldId::Description => f.write_str("descriptionAddIssueModal"), + IssueFieldId::Reporter => f.write_str("reporterAddIssueModal"), + IssueFieldId::Assignees => f.write_str("assigneesAddIssueModal"), + IssueFieldId::Priority => f.write_str("issuePriorityAddIssueModal"), + IssueFieldId::Status => f.write_str("addIssueModal-status"), + IssueFieldId::Estimate => f.write_str("addIssueModal-estimate"), + IssueFieldId::TimeSpend => f.write_str("addIssueModal-timeSpend"), + IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"), + IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"), }, FieldId::TextFilterBoard => f.write_str("textFilterBoard"), FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"), FieldId::ProjectSettings(sub) => match sub { - ProjectSettingsFieldId::Name => f.write_str("projectSettings-name"), - ProjectSettingsFieldId::Url => f.write_str("projectSettings-url"), - ProjectSettingsFieldId::Description => f.write_str("projectSettings-description"), - ProjectSettingsFieldId::Category => f.write_str("projectSettings-category"), + ProjectFieldId::Name => f.write_str("projectSettings-name"), + ProjectFieldId::Url => f.write_str("projectSettings-url"), + ProjectFieldId::Description => f.write_str("projectSettings-description"), + ProjectFieldId::Category => f.write_str("projectSettings-category"), }, FieldId::Login(sub) => match sub { LoginFieldId::Email => f.write_str("login-email"), @@ -332,8 +327,8 @@ pub fn render() { #[inline] fn authorize_or_redirect() { match crate::shared::read_auth_token() { - Ok(uuid) => { - send_ws_msg(WsMsg::AuthorizeRequest(uuid)); + Ok(token) => { + send_ws_msg(WsMsg::AuthorizeRequest(token)); } Err(..) => { let pathname = seed::document().location().unwrap().pathname().unwrap(); diff --git a/jirs-client/src/modal/add_issue.rs b/jirs-client/src/modal/add_issue.rs index 2e620f80..aac5bc82 100644 --- a/jirs-client/src/modal/add_issue.rs +++ b/jirs-client/src/modal/add_issue.rs @@ -1,5 +1,6 @@ use seed::{prelude::*, *}; +use jirs_data::IssueFieldId; use jirs_data::{IssuePriority, IssueType, ToVec}; use crate::api::send_ws_msg; @@ -14,7 +15,7 @@ use crate::shared::styled_select::StyledSelectChange; use crate::shared::styled_select_child::ToStyledSelectChild; use crate::shared::styled_textarea::StyledTextarea; use crate::shared::ToNode; -use crate::{AddIssueModalFieldId, FieldId, Msg}; +use crate::{FieldId, Msg}; pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { let modal = model.modals.iter_mut().find(|modal| match modal { @@ -57,16 +58,16 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde orders.skip().send_msg(Msg::ModalDropped); } - Msg::InputChanged(FieldId::AddIssueModal(AddIssueModalFieldId::Description), value) => { + Msg::InputChanged(FieldId::AddIssueModal(IssueFieldId::Description), value) => { modal.description = Some(value.clone()); } - Msg::InputChanged(FieldId::AddIssueModal(AddIssueModalFieldId::Summary), value) => { + Msg::InputChanged(FieldId::AddIssueModal(IssueFieldId::Title), value) => { modal.title = value.clone(); } // IssueTypeAddIssueModal Msg::StyledSelectChanged( - FieldId::AddIssueModal(AddIssueModalFieldId::IssueType), + FieldId::AddIssueModal(IssueFieldId::Type), StyledSelectChange::Changed(id), ) => { modal.issue_type = (*id).into(); @@ -74,7 +75,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde // ReporterAddIssueModal Msg::StyledSelectChanged( - FieldId::AddIssueModal(AddIssueModalFieldId::Reporter), + FieldId::AddIssueModal(IssueFieldId::Reporter), StyledSelectChange::Changed(id), ) => { modal.reporter_id = Some(*id as i32); @@ -82,7 +83,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde // AssigneesAddIssueModal Msg::StyledSelectChanged( - FieldId::AddIssueModal(AddIssueModalFieldId::Assignees), + FieldId::AddIssueModal(IssueFieldId::Assignees), StyledSelectChange::Changed(id), ) => { let id = *id as i32; @@ -91,7 +92,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde } } Msg::StyledSelectChanged( - FieldId::AddIssueModal(AddIssueModalFieldId::Assignees), + FieldId::AddIssueModal(IssueFieldId::Assignees), StyledSelectChange::RemoveMulti(id), ) => { let id = *id as i32; @@ -106,7 +107,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde // IssuePriorityAddIssueModal Msg::StyledSelectChanged( - FieldId::AddIssueModal(AddIssueModalFieldId::Priority), + FieldId::AddIssueModal(IssueFieldId::Priority), StyledSelectChange::Changed(id), ) => { modal.priority = (*id).into(); @@ -117,7 +118,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde } pub fn view(model: &Model, modal: &AddIssueModal) -> Node { - let select_type = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::IssueType)) + let select_type = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Type)) .name("type") .normal() .text_filter(modal.type_state.text_filter.as_str()) @@ -139,7 +140,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { .build() .into_node(); - let short_summary = StyledInput::build(FieldId::AddIssueModal(AddIssueModalFieldId::Summary)) + let short_summary = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title)) .valid(true) .build() .into_node(); @@ -152,7 +153,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { let description = StyledTextarea::build() .height(110) - .build(FieldId::AddIssueModal(AddIssueModalFieldId::Description)) + .build(FieldId::AddIssueModal(IssueFieldId::Description)) .into_node(); let description_field = StyledField::build() .label("Description") @@ -165,7 +166,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { .reporter_id .or_else(|| model.user.as_ref().map(|u| u.id)) .unwrap_or_default(); - let reporter = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Reporter)) + let reporter = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Reporter)) .normal() .text_filter(modal.reporter_state.text_filter.as_str()) .opened(modal.reporter_state.opened) @@ -199,7 +200,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { .build() .into_node(); - let assignees = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Assignees)) + let assignees = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Assignees)) .normal() .multi() .text_filter(modal.assignees_state.text_filter.as_str()) @@ -234,22 +235,21 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { .build() .into_node(); - let select_priority = - StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Priority)) - .name("priority") - .normal() - .text_filter(modal.priority_state.text_filter.as_str()) - .opened(modal.priority_state.opened) - .valid(true) - .options( - IssuePriority::ordered() - .iter() - .map(|p| p.to_select_child().name("priority")) - .collect(), - ) - .selected(vec![modal.priority.to_select_child().name("priority")]) - .build() - .into_node(); + let select_priority = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Priority)) + .name("priority") + .normal() + .text_filter(modal.priority_state.text_filter.as_str()) + .opened(modal.priority_state.opened) + .valid(true) + .options( + IssuePriority::ordered() + .iter() + .map(|p| p.to_select_child().name("priority")) + .collect(), + ) + .selected(vec![modal.priority.to_select_child().name("priority")]) + .build() + .into_node(); let issue_priority_field = StyledField::build() .label("Issue Type") .tip("Priority in relation to other issues.") diff --git a/jirs-client/src/modal/issue_details.rs b/jirs-client/src/modal/issue_details.rs index 3765ba87..7c1078a0 100644 --- a/jirs-client/src/modal/issue_details.rs +++ b/jirs-client/src/modal/issue_details.rs @@ -15,7 +15,7 @@ use crate::shared::styled_select_child::ToStyledSelectChild; use crate::shared::styled_textarea::StyledTextarea; use crate::shared::tracking_widget::tracking_link; use crate::shared::ToNode; -use crate::{EditIssueModalFieldId, FieldChange, FieldId, Msg}; +use crate::{EditIssueModalSection, FieldChange, FieldId, Msg}; pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { let modal: &mut EditIssueModal = match model.modals.get_mut(0) { @@ -33,35 +33,51 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { modal.payload = issue.clone().into(); } Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalFieldId::IssueType), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), StyledSelectChange::Changed(value), ) => { modal.payload.issue_type = (*value).into(); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::Type, + PayloadVariant::IssueType(modal.payload.issue_type.clone()), + )); } Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalFieldId::Status), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Status)), StyledSelectChange::Changed(value), ) => { modal.payload.status = (*value).into(); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::Status, + PayloadVariant::IssueStatus(modal.payload.status.clone()), + )); } Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalFieldId::Reporter), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)), StyledSelectChange::Changed(value), ) => { modal.payload.reporter_id = *value as i32; - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::Reporter, + PayloadVariant::I32(modal.payload.reporter_id), + )); } Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalFieldId::Assignees), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)), StyledSelectChange::Changed(value), ) => { modal.payload.user_ids.push(*value as i32); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::Assignees, + PayloadVariant::VecI32(modal.payload.user_ids.clone()), + )); } Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalFieldId::Assignees), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)), StyledSelectChange::RemoveMulti(value), ) => { let mut old = vec![]; @@ -72,40 +88,83 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { modal.payload.user_ids.push(id); } } - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::Assignees, + PayloadVariant::VecI32(modal.payload.user_ids.clone()), + )); } Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalFieldId::Priority), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)), StyledSelectChange::Changed(value), ) => { modal.payload.priority = (*value).into(); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::Priority, + PayloadVariant::IssuePriority(modal.payload.priority), + )); } - Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Title), value) => { + Msg::InputChanged( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)), + value, + ) => { modal.payload.title = value.clone(); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::Title, + PayloadVariant::String(modal.payload.title.clone()), + )); } - Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Description), value) => { + Msg::InputChanged( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)), + value, + ) => { modal.payload.description = Some(value.clone()); modal.payload.description_text = Some(value.clone()); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::Description, + PayloadVariant::String( + modal + .payload + .description + .as_ref() + .cloned() + .unwrap_or_default(), + ), + )); } - Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::TimeSpend), value) => { + Msg::InputChanged( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpend)), + value, + ) => { modal.payload.time_spent = value.parse::().ok(); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::TimeSpend, + PayloadVariant::OptionI32(modal.payload.time_spent.clone()), + )); } - Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::TimeRemaining), value) => { + Msg::InputChanged( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)), + value, + ) => { modal.payload.time_remaining = value.parse::().ok(); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::TimeRemaining, + PayloadVariant::OptionI32(modal.payload.time_remaining.clone()), + )); } Msg::ModalChanged(FieldChange::TabChanged( - FieldId::EditIssueModal(EditIssueModalFieldId::Description), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)), mode, )) => { modal.description_editor_mode = mode.clone(); } Msg::ModalChanged(FieldChange::ToggleCommentForm( - FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), flag, )) => { modal.comment_form.creating = *flag; @@ -115,22 +174,34 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { } } // comments - Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), text) => { + Msg::InputChanged( + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), + text, + ) => { modal.comment_form.body = text.clone(); } - Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate), value) => { - match value.parse::() { - Ok(n) if !value.is_empty() => { - modal.payload.estimate = Some(n); - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); - } - _ if value.is_empty() => { - modal.payload.estimate = None; - send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); - } - _ => {} + Msg::InputChanged( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)), + value, + ) => match value.parse::() { + Ok(n) if !value.is_empty() => { + modal.payload.estimate = Some(n); + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::TimeRemaining, + PayloadVariant::OptionI32(modal.payload.estimate.clone()), + )); } - } + _ if value.is_empty() => { + modal.payload.estimate = None; + send_ws_msg(WsMsg::IssueUpdateRequest( + modal.id, + IssueFieldId::TimeRemaining, + PayloadVariant::OptionI32(modal.payload.estimate.clone()), + )); + } + _ => {} + }, Msg::SaveComment => { let msg = match modal.comment_form.id { Some(id) => WsMsg::UpdateComment(UpdateCommentPayload { @@ -147,12 +218,12 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders .skip() .send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm( - FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), false, ))); } Msg::ModalChanged(FieldChange::EditComment( - FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), comment_id, )) => { let id = *comment_id; @@ -176,7 +247,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders .skip() .send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm( - FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), true, ))); } @@ -256,29 +327,30 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node { .build() .into_node(); - let issue_type_select = - StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::IssueType)) - .dropdown_width(150) + let issue_type_select = StyledSelect::build(FieldId::EditIssueModal( + EditIssueModalSection::Issue(IssueFieldId::Type), + )) + .dropdown_width(150) + .name("type") + .text_filter(top_type_state.text_filter.as_str()) + .opened(top_type_state.opened) + .valid(true) + .options( + IssueType::ordered() + .into_iter() + .map(|t| t.to_select_child().name("type")) + .collect(), + ) + .selected(vec![{ + let id = modal.id; + let issue_type = &payload.issue_type; + issue_type + .to_select_child() .name("type") - .text_filter(top_type_state.text_filter.as_str()) - .opened(top_type_state.opened) - .valid(true) - .options( - IssueType::ordered() - .into_iter() - .map(|t| t.to_select_child().name("type")) - .collect(), - ) - .selected(vec![{ - let id = modal.id; - let issue_type = &payload.issue_type; - issue_type - .to_select_child() - .name("type") - .text(format!("{} - {}", issue_type, id)) - }]) - .build() - .into_node(); + .text(format!("{} - {}", issue_type, id)) + }]) + .build() + .into_node(); div![ attrs![At::Class => "topActions"], @@ -305,17 +377,20 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { .add_class("textarea") .max_height(48) .height(0) - .build(FieldId::EditIssueModal(EditIssueModalFieldId::Title)) + .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Title, + ))) .into_node(); let description_text = payload.description.as_ref().cloned().unwrap_or_default(); - let description = - StyledEditor::build(FieldId::EditIssueModal(EditIssueModalFieldId::Description)) - .text(description_text) - .mode(description_editor_mode.clone()) - .update_on(Ev::Change) - .build() - .into_node(); + let description = StyledEditor::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Description, + ))) + .text(description_text) + .mode(description_editor_mode.clone()) + .update_on(Ev::Change) + .build() + .into_node(); let description_field = StyledField::build().input(description).build().into_node(); let user_avatar = StyledAvatar::build() @@ -338,7 +413,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { let handler = mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); Msg::ModalChanged(FieldChange::ToggleCommentForm( - FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), !creating_comment, )) }); @@ -388,7 +463,7 @@ fn build_comment_form(form: &CommentForm) -> Vec> { let close_comment_form = mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); Msg::ModalChanged(FieldChange::ToggleCommentForm( - FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), false, )) }); @@ -396,7 +471,9 @@ fn build_comment_form(form: &CommentForm) -> Vec> { let text_area = StyledTextarea::build() .value(form.body.as_str()) .placeholder("Add a comment...") - .build(FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody)) + .build(FieldId::EditIssueModal(EditIssueModalSection::Comment( + CommentFieldId::Body, + ))) .into_node(); let submit = StyledButton::build() @@ -439,7 +516,7 @@ fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Option Node { .. } = modal; - let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Status)) - .name("status") - .opened(status_state.opened) - .normal() - .text_filter(status_state.text_filter.as_str()) - .options( - IssueStatus::ordered() - .into_iter() - .map(|opt| opt.to_select_child().name("status")) - .collect(), - ) - .selected(vec![payload.status.to_select_child().name("status")]) - .valid(true) - .build() - .into_node(); + let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Status, + ))) + .name("status") + .opened(status_state.opened) + .normal() + .text_filter(status_state.text_filter.as_str()) + .options( + IssueStatus::ordered() + .into_iter() + .map(|opt| opt.to_select_child().name("status")) + .collect(), + ) + .selected(vec![payload.status.to_select_child().name("status")]) + .valid(true) + .build() + .into_node(); let status_field = StyledField::build() .input(status) .label("Status") .build() .into_node(); - let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Assignees)) - .name("assignees") - .opened(assignees_state.opened) - .empty() - .multi() - .text_filter(assignees_state.text_filter.as_str()) - .options( - model - .users - .iter() - .map(|user| user.to_select_child().name("assignees")) - .collect(), - ) - .selected( - model - .users - .iter() - .filter(|user| payload.user_ids.contains(&user.id)) - .map(|user| user.to_select_child().name("assignees")) - .collect(), - ) - .build() - .into_node(); + let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Assignees, + ))) + .name("assignees") + .opened(assignees_state.opened) + .empty() + .multi() + .text_filter(assignees_state.text_filter.as_str()) + .options( + model + .users + .iter() + .map(|user| user.to_select_child().name("assignees")) + .collect(), + ) + .selected( + model + .users + .iter() + .filter(|user| payload.user_ids.contains(&user.id)) + .map(|user| user.to_select_child().name("assignees")) + .collect(), + ) + .build() + .into_node(); let assignees_field = StyledField::build() .input(assignees) .label("Assignees") .build() .into_node(); - let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Reporter)) - .name("reporter") - .opened(reporter_state.opened) - .empty() - .text_filter(reporter_state.text_filter.as_str()) - .options( - model - .users - .iter() - .map(|user| user.to_select_child().name("reporter")) - .collect(), - ) - .selected( - model - .users - .iter() - .filter(|user| payload.reporter_id == user.id) - .map(|user| user.to_select_child().name("reporter")) - .collect(), - ) - .build() - .into_node(); + let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Reporter, + ))) + .name("reporter") + .opened(reporter_state.opened) + .empty() + .text_filter(reporter_state.text_filter.as_str()) + .options( + model + .users + .iter() + .map(|user| user.to_select_child().name("reporter")) + .collect(), + ) + .selected( + model + .users + .iter() + .filter(|user| payload.reporter_id == user.id) + .map(|user| user.to_select_child().name("reporter")) + .collect(), + ) + .build() + .into_node(); let reporter_field = StyledField::build() .input(reporter) .label("Reporter") .build() .into_node(); - let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Priority)) - .name("priority") - .opened(priority_state.opened) - .empty() - .text_filter(priority_state.text_filter.as_str()) - .options( - IssuePriority::ordered() - .into_iter() - .map(|p| p.to_select_child().name("priority")) - .collect(), - ) - .selected(vec![payload.priority.to_select_child().name("priority")]) - .build() - .into_node(); + let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Priority, + ))) + .name("priority") + .opened(priority_state.opened) + .empty() + .text_filter(priority_state.text_filter.as_str()) + .options( + IssuePriority::ordered() + .into_iter() + .map(|p| p.to_select_child().name("priority")) + .collect(), + ) + .selected(vec![payload.priority.to_select_child().name("priority")]) + .build() + .into_node(); let priority_field = StyledField::build() .input(priority) .label("Priority") .build() .into_node(); - let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate)) - .valid(true) - .value( - payload - .estimate - .as_ref() - .map(|n| n.to_string()) - .unwrap_or_default(), - ) - .build() - .into_node(); + let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Estimate, + ))) + .valid(true) + .value( + payload + .estimate + .as_ref() + .map(|n| n.to_string()) + .unwrap_or_default(), + ) + .build() + .into_node(); let estimate_field = StyledField::build() .input(estimate) .label("Original Estimate (hours)") diff --git a/jirs-client/src/modal/time_tracking.rs b/jirs-client/src/modal/time_tracking.rs index 129eed5e..b11a2f94 100644 --- a/jirs-client/src/modal/time_tracking.rs +++ b/jirs-client/src/modal/time_tracking.rs @@ -1,6 +1,6 @@ use seed::{prelude::*, *}; -use jirs_data::IssueId; +use jirs_data::{IssueFieldId, IssueId}; use crate::model::{ModalType, Model}; use crate::shared::styled_button::StyledButton; @@ -9,7 +9,7 @@ use crate::shared::styled_input::StyledInput; use crate::shared::styled_modal::StyledModal; use crate::shared::tracking_widget::tracking_widget; use crate::shared::{find_issue, ToNode}; -use crate::{EditIssueModalFieldId, FieldId, Msg}; +use crate::{EditIssueModalSection, FieldId, Msg}; pub fn view(model: &Model, issue_id: IssueId) -> Node { let _issue = match find_issue(model, issue_id) { @@ -26,26 +26,28 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node { let tracking = tracking_widget(model, edit_issue_modal); - let time_spent = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::TimeSpend)) - .value( - edit_issue_modal - .payload - .time_spent - .as_ref() - .map(|n| n.to_string()) - .unwrap_or_default(), - ) - .valid(true) - .build() - .into_node(); + let time_spent = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::TimeSpend, + ))) + .value( + edit_issue_modal + .payload + .time_spent + .as_ref() + .map(|n| n.to_string()) + .unwrap_or_default(), + ) + .valid(true) + .build() + .into_node(); let time_spent_field = StyledField::build() .input(time_spent) .label("Time spent") .build() .into_node(); - let time_remaining = StyledInput::build(FieldId::EditIssueModal( - EditIssueModalFieldId::TimeRemaining, - )) + let time_remaining = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::TimeRemaining, + ))) .value( edit_issue_modal .payload diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index db3593b1..5bbf7a55 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -7,9 +7,7 @@ use jirs_data::*; use crate::shared::styled_editor::Mode; use crate::shared::styled_select::StyledSelectState; -use crate::{ - AddIssueModalFieldId, EditIssueModalFieldId, FieldId, ProjectSettingsFieldId, HOST_URL, -}; +use crate::{EditIssueModalSection, FieldId, ProjectFieldId, HOST_URL}; #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] pub enum ModalType { @@ -65,19 +63,19 @@ impl EditIssueModal { user_ids: issue.user_ids.clone(), }, top_type_state: StyledSelectState::new(FieldId::EditIssueModal( - EditIssueModalFieldId::IssueType, + EditIssueModalSection::Issue(IssueFieldId::Type), )), status_state: StyledSelectState::new(FieldId::EditIssueModal( - EditIssueModalFieldId::Status, + EditIssueModalSection::Issue(IssueFieldId::Status), )), reporter_state: StyledSelectState::new(FieldId::EditIssueModal( - EditIssueModalFieldId::Reporter, + EditIssueModalSection::Issue(IssueFieldId::Reporter), )), assignees_state: StyledSelectState::new(FieldId::EditIssueModal( - EditIssueModalFieldId::Assignees, + EditIssueModalSection::Issue(IssueFieldId::Assignees), )), priority_state: StyledSelectState::new(FieldId::EditIssueModal( - EditIssueModalFieldId::Priority, + EditIssueModalSection::Issue(IssueFieldId::Priority), )), description_editor_mode: Mode::View, comment_form: CommentForm { @@ -126,18 +124,12 @@ impl Default for AddIssueModal { project_id: Default::default(), user_ids: Default::default(), reporter_id: Default::default(), - type_state: StyledSelectState::new(FieldId::AddIssueModal( - AddIssueModalFieldId::IssueType, - )), - reporter_state: StyledSelectState::new(FieldId::AddIssueModal( - AddIssueModalFieldId::Reporter, - )), + type_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Type)), + reporter_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Reporter)), assignees_state: StyledSelectState::new(FieldId::AddIssueModal( - AddIssueModalFieldId::Assignees, - )), - priority_state: StyledSelectState::new(FieldId::AddIssueModal( - AddIssueModalFieldId::Priority, + IssueFieldId::Assignees, )), + priority_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Priority)), } } } @@ -220,7 +212,7 @@ impl ProjectSettingsPage { }, description_mode: EditorMode::View, project_category_state: StyledSelectState::new(FieldId::ProjectSettings( - ProjectSettingsFieldId::Category, + ProjectFieldId::Category, )), } } diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index a1c7dc31..a4bbf13d 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -11,7 +11,7 @@ use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_input::StyledInput; use crate::shared::styled_select::StyledSelectChange; use crate::shared::{drag_ev, inner_layout, ToNode}; -use crate::{EditIssueModalFieldId, FieldId, Msg}; +use crate::{EditIssueModalSection, FieldId, Msg}; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { if model.user.is_none() { @@ -63,7 +63,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order orders.skip().send_msg(Msg::ModalDropped); } Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalFieldId::IssueType), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), StyledSelectChange::Text(text), ) => { let modal = model diff --git a/jirs-client/src/project_settings.rs b/jirs-client/src/project_settings.rs index 5d9804a0..59ae1e93 100644 --- a/jirs-client/src/project_settings.rs +++ b/jirs-client/src/project_settings.rs @@ -13,7 +13,7 @@ use crate::shared::styled_select_child::ToStyledSelectChild; use crate::shared::styled_textarea::StyledTextarea; use crate::shared::{inner_layout, ToNode}; use crate::FieldChange::TabChanged; -use crate::{model, FieldId, Msg, ProjectSettingsFieldId}; +use crate::{model, FieldId, Msg, ProjectFieldId}; pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { if model.user.is_none() { @@ -45,24 +45,24 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) page.project_category_state.update(&msg, orders); match msg { Msg::ProjectSaveChanges => send_ws_msg(WsMsg::ProjectUpdateRequest(page.payload.clone())), - Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Name), text) => { + Msg::InputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => { page.payload.name = Some(text); } - Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Url), text) => { + Msg::InputChanged(FieldId::ProjectSettings(ProjectFieldId::Url), text) => { page.payload.url = Some(text); } - Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Description), text) => { + Msg::InputChanged(FieldId::ProjectSettings(ProjectFieldId::Description), text) => { page.payload.description = Some(text); } Msg::StyledSelectChanged( - FieldId::ProjectSettings(ProjectSettingsFieldId::Category), + FieldId::ProjectSettings(ProjectFieldId::Category), StyledSelectChange::Changed(value), ) => { let category = value.into(); page.payload.category = Some(category); } Msg::ModalChanged(TabChanged( - FieldId::ProjectSettings(ProjectSettingsFieldId::Description), + FieldId::ProjectSettings(ProjectFieldId::Description), mode, )) => { page.description_mode = mode; @@ -89,7 +89,7 @@ pub fn view(model: &model::Model) -> Node { .height(39) .max_height(39) .disable_auto_resize() - .build(FieldId::ProjectSettings(ProjectSettingsFieldId::Name)) + .build(FieldId::ProjectSettings(ProjectFieldId::Name)) .into_node(); let name_field = StyledField::build() .label("Name") @@ -103,7 +103,7 @@ pub fn view(model: &model::Model) -> Node { .max_height(39) .disable_auto_resize() .value(page.payload.url.as_ref().cloned().unwrap_or_default()) - .build(FieldId::ProjectSettings(ProjectSettingsFieldId::Url)) + .build(FieldId::ProjectSettings(ProjectFieldId::Url)) .into_node(); let url_field = StyledField::build() .label("Url") @@ -112,20 +112,18 @@ pub fn view(model: &model::Model) -> Node { .build() .into_node(); - let description = StyledEditor::build(FieldId::ProjectSettings( - ProjectSettingsFieldId::Description, - )) - .text( - page.payload - .description - .as_ref() - .cloned() - .unwrap_or_default(), - ) - .update_on(Ev::Change) - .mode(page.description_mode.clone()) - .build() - .into_node(); + let description = StyledEditor::build(FieldId::ProjectSettings(ProjectFieldId::Description)) + .text( + page.payload + .description + .as_ref() + .cloned() + .unwrap_or_default(), + ) + .update_on(Ev::Change) + .mode(page.description_mode.clone()) + .build() + .into_node(); let description_field = StyledField::build() .input(description) .label("Description") @@ -133,7 +131,7 @@ pub fn view(model: &model::Model) -> Node { .build() .into_node(); - let category = StyledSelect::build(FieldId::ProjectSettings(ProjectSettingsFieldId::Category)) + let category = StyledSelect::build(FieldId::ProjectSettings(ProjectFieldId::Category)) .opened(page.project_category_state.opened) .text_filter(page.project_category_state.text_filter.as_str()) .valid(true) diff --git a/jirs-client/src/ws/issue.rs b/jirs-client/src/ws/issue.rs index f5486442..bb080634 100644 --- a/jirs-client/src/ws/issue.rs +++ b/jirs-client/src/ws/issue.rs @@ -93,23 +93,16 @@ pub fn sync(model: &mut Model) { continue; } - let payload = UpdateIssuePayload { - title: issue.title.clone(), - issue_type: issue.issue_type.clone(), - status: issue.status.clone(), - priority: issue.priority.clone(), - list_position: issue.list_position, - description: issue.description.clone(), - description_text: issue.description_text.clone(), - estimate: issue.estimate, - time_spent: issue.time_spent, - time_remaining: issue.time_remaining, - project_id: issue.project_id, - reporter_id: issue.reporter_id, - user_ids: issue.user_ids.clone(), - }; - - send_ws_msg(WsMsg::IssueUpdateRequest(issue.id, payload)); + send_ws_msg(WsMsg::IssueUpdateRequest( + issue.id, + IssueFieldId::Status, + PayloadVariant::IssueStatus(issue.status.clone()), + )); + send_ws_msg(WsMsg::IssueUpdateRequest( + issue.id, + IssueFieldId::ListPosition, + PayloadVariant::I32(issue.list_position), + )); } project_page.dragged_issue_id = None; project_page.last_drag_exchange_id = None; diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 37eeb14c..592f41b5 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -168,7 +168,7 @@ impl IssueStatus { #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "IssuePriorityType")] -#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] +#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] pub enum IssuePriority { Highest, High, @@ -463,6 +463,53 @@ pub struct UpdateProjectPayload { pub category: Option, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum PayloadVariant { + OptionI32(Option), + VecI32(Vec), + I32(i32), + String(String), + IssueType(IssueType), + IssueStatus(IssueStatus), + IssuePriority(IssuePriority), + ProjectCategory(ProjectCategory), +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] +pub enum ProjectFieldId { + Name, + Url, + Description, + Category, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] +pub enum LoginFieldId { + Username, + Email, + Token, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] +pub enum CommentFieldId { + Body, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] +pub enum IssueFieldId { + Type, + Title, + Description, + Status, + ListPosition, + Assignees, + Reporter, + Priority, + Estimate, + TimeSpend, + TimeRemaining, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum WsMsg { Ping, @@ -488,7 +535,7 @@ pub enum WsMsg { ProjectUpdateRequest(UpdateProjectPayload), // issue - IssueUpdateRequest(IssueId, UpdateIssuePayload), + IssueUpdateRequest(IssueId, IssueFieldId, PayloadVariant), IssueUpdated(Issue), IssueDeleteRequest(IssueId), IssueDeleted(IssueId), diff --git a/jirs-server/Cargo.toml b/jirs-server/Cargo.toml index f06cba64..2edd99c6 100644 --- a/jirs-server/Cargo.toml +++ b/jirs-server/Cargo.toml @@ -28,6 +28,7 @@ libc = { version = "0.2.0" } pq-sys = { version = ">=0.3.0, <0.5.0" } quickcheck = { version = "0.4" } serde_json = { version = ">=0.8.0, <2.0" } +toml = "0.5" bincode = "1.2.1" time = { version = "0.1" } url = { version = "2.1.0" } @@ -44,6 +45,8 @@ log = "0.4" pretty_env_logger = "0.4" env_logger = "0.7" futures = { version = "*" } +lettre = { version = "*" } +lettre_email = { version = "*" } [dependencies.diesel] version = "1.4.4" diff --git a/jirs-server/src/db/authorize_user.rs b/jirs-server/src/db/authorize_user.rs index fe09711b..ce7c244e 100644 --- a/jirs-server/src/db/authorize_user.rs +++ b/jirs-server/src/db/authorize_user.rs @@ -23,7 +23,7 @@ impl Handler for DbExecutor { use crate::schema::users::dsl::{id, users}; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let token = tokens diff --git a/jirs-server/src/db/comments.rs b/jirs-server/src/db/comments.rs index 391dfb7d..f1e2bff6 100644 --- a/jirs-server/src/db/comments.rs +++ b/jirs-server/src/db/comments.rs @@ -22,7 +22,7 @@ impl Handler for DbExecutor { use crate::schema::comments::dsl::*; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let rows: Vec = comments @@ -59,7 +59,7 @@ impl Handler for DbExecutor { }; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let row: Comment = diesel::insert_into(comments) @@ -88,7 +88,7 @@ impl Handler for DbExecutor { use crate::schema::comments::dsl::*; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let query = diesel::update( @@ -122,7 +122,7 @@ impl Handler for DbExecutor { use crate::schema::comments::dsl::*; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; diesel::delete( diff --git a/jirs-server/src/db/issue_assignees.rs b/jirs-server/src/db/issue_assignees.rs index 2e89f21a..715852bc 100644 --- a/jirs-server/src/db/issue_assignees.rs +++ b/jirs-server/src/db/issue_assignees.rs @@ -22,7 +22,7 @@ impl Handler for DbExecutor { use crate::schema::issue_assignees::dsl::*; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; issue_assignees diff --git a/jirs-server/src/db/issues.rs b/jirs-server/src/db/issues.rs index 4ddb08c7..ea7a1fd7 100644 --- a/jirs-server/src/db/issues.rs +++ b/jirs-server/src/db/issues.rs @@ -27,7 +27,7 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: LoadIssue, _ctx: &mut Self::Context) -> Self::Result { use crate::schema::issues::dsl::{id, issues}; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let record = issues @@ -54,7 +54,7 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: LoadProjectIssues, _ctx: &mut Self::Context) -> Self::Result { use crate::schema::issues::dsl::{issues, project_id}; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let chain = issues.filter(project_id.eq(msg.project_id)).distinct(); @@ -69,7 +69,7 @@ impl Handler for DbExecutor { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Default)] pub struct UpdateIssue { pub issue_id: i32, pub title: Option, @@ -97,7 +97,7 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: UpdateIssue, _ctx: &mut Self::Context) -> Self::Result { use crate::schema::issues::dsl::{self, issues}; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; @@ -187,7 +187,7 @@ impl Handler for DbExecutor { use crate::schema::issues::dsl::issues; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; @@ -229,7 +229,7 @@ impl Handler for DbExecutor { use crate::schema::issues::dsl::{issues, status}; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; diff --git a/jirs-server/src/db/mod.rs b/jirs-server/src/db/mod.rs index da75f843..4b0f6612 100644 --- a/jirs-server/src/db/mod.rs +++ b/jirs-server/src/db/mod.rs @@ -1,7 +1,10 @@ +use std::fs::*; + use actix::{Actor, SyncContext}; #[cfg(not(debug_assertions))] use diesel::pg::PgConnection; use diesel::r2d2::{self, ConnectionManager}; +use serde::{Deserialize, Serialize}; #[cfg(debug_assertions)] use crate::db::dev::VerboseConnection; @@ -19,7 +22,10 @@ pub type DbPool = r2d2::Pool>; #[cfg(not(debug_assertions))] pub type DbPool = r2d2::Pool>; -pub struct DbExecutor(pub DbPool); +pub struct DbExecutor { + pub pool: DbPool, + pub config: Configuration, +} impl Actor for DbExecutor { type Context = SyncContext; @@ -27,7 +33,10 @@ impl Actor for DbExecutor { impl Default for DbExecutor { fn default() -> Self { - Self(build_pool()) + Self { + pool: build_pool(), + config: Configuration::read(), + } } } @@ -126,3 +135,42 @@ pub mod dev { } } } + +#[derive(Serialize, Deserialize)] +pub struct Configuration { + pub concurrency: usize, + pub database_url: String, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + concurrency: 2, + database_url: "postgres://postgres@localhost:5432/jirs".to_string(), + } + } +} + +impl Configuration { + pub fn read() -> Self { + let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); + match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => { + let config = Configuration::default(); + config.write().unwrap_or_else(|e| panic!(e)); + config + } + } + } + + pub fn write(&self) -> Result<(), String> { + let s = toml::to_string(self).map_err(|e| e.to_string())?; + write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; + Ok(()) + } + + pub fn config_file() -> &'static str { + "db.toml" + } +} diff --git a/jirs-server/src/db/projects.rs b/jirs-server/src/db/projects.rs index c5a130cd..7b4b05b8 100644 --- a/jirs-server/src/db/projects.rs +++ b/jirs-server/src/db/projects.rs @@ -23,7 +23,7 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: LoadCurrentProject, _ctx: &mut Self::Context) -> Self::Result { use crate::schema::projects::dsl::{id, projects}; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; @@ -59,7 +59,7 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: UpdateProject, _ctx: &mut Self::Context) -> Self::Result { use crate::schema::projects::dsl::*; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; diff --git a/jirs-server/src/db/tokens.rs b/jirs-server/src/db/tokens.rs index 26f45310..45c20656 100644 --- a/jirs-server/src/db/tokens.rs +++ b/jirs-server/src/db/tokens.rs @@ -24,7 +24,7 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: FindBindToken, _: &mut Self::Context) -> Self::Result { use crate::schema::tokens::dsl::{bind_token, tokens}; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; @@ -58,7 +58,7 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: CreateBindToken, _: &mut Self::Context) -> Self::Result { use crate::schema::tokens::dsl::tokens; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; diff --git a/jirs-server/src/db/users.rs b/jirs-server/src/db/users.rs index a300ee72..a49ee7a1 100644 --- a/jirs-server/src/db/users.rs +++ b/jirs-server/src/db/users.rs @@ -1,10 +1,11 @@ -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; -use crate::models::{IssueAssignee, User}; use actix::{Handler, Message}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; +use crate::db::DbExecutor; +use crate::errors::ServiceErrors; +use crate::models::{IssueAssignee, User}; + #[derive(Serialize, Deserialize, Debug)] pub struct FindUser { pub name: String, @@ -22,7 +23,7 @@ impl Handler for DbExecutor { use crate::schema::users::dsl::*; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let row: User = users @@ -53,7 +54,7 @@ impl Handler for DbExecutor { use crate::schema::users::dsl::*; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let rows: Vec = users @@ -82,7 +83,7 @@ impl Handler for DbExecutor { use crate::schema::users::dsl::*; let conn = &self - .0 + .pool .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let rows: Vec<(User, IssueAssignee)> = users diff --git a/jirs-server/src/mail/mod.rs b/jirs-server/src/mail/mod.rs new file mode 100644 index 00000000..cf931979 --- /dev/null +++ b/jirs-server/src/mail/mod.rs @@ -0,0 +1,92 @@ +use std::fs::*; + +use actix::{Actor, SyncContext}; +use lettre; +use serde::{Deserialize, Serialize}; + +pub mod welcome; + +pub type MailTransport = lettre::SmtpTransport; + +pub struct MailExecutor { + pub transport: MailTransport, + pub config: Configuration, +} + +impl Actor for MailExecutor { + type Context = SyncContext; +} + +impl Default for MailExecutor { + fn default() -> Self { + let config = Configuration::read(); + Self { + transport: mail_transport(&config), + config, + } + } +} + +fn mail_client(config: &Configuration) -> lettre::SmtpClient { + let mail_user = config.user.as_str(); + let mail_pass = config.pass.as_str(); + let mail_host = config.host.as_str(); + + lettre::SmtpClient::new_simple(mail_host) + .expect("Failed to init SMTP client") + .credentials(lettre::smtp::authentication::Credentials::new( + mail_user.to_string(), + mail_pass.to_string(), + )) + .connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited) + .smtp_utf8(true) +} + +fn mail_transport(config: &Configuration) -> MailTransport { + mail_client(config).transport() +} + +#[derive(Serialize, Deserialize)] +pub struct Configuration { + pub concurrency: usize, + pub user: String, + pub pass: String, + pub host: String, + pub from: String, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + concurrency: 2, + user: "apikey".to_string(), + pass: "YOUR-TOKEN".to_string(), + host: "smtp.sendgrid.net".to_string(), + from: "contact@jirs.pl".to_string(), + } + } +} + +impl Configuration { + pub fn read() -> Self { + let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); + match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => { + let config = Configuration::default(); + config.write().unwrap_or_else(|e| panic!(e)); + config + } + } + } + + pub fn write(&self) -> Result<(), String> { + let s = toml::to_string(self).map_err(|e| e.to_string())?; + write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; + Ok(()) + } + + fn config_file() -> &'static str { + "mail.toml" + } +} diff --git a/jirs-server/src/mail/welcome.rs b/jirs-server/src/mail/welcome.rs new file mode 100644 index 00000000..7c64bfc9 --- /dev/null +++ b/jirs-server/src/mail/welcome.rs @@ -0,0 +1,60 @@ +use actix::{Handler, Message}; +use lettre; +use lettre_email; +use uuid::Uuid; + +use crate::mail::MailExecutor; + +#[derive(Debug)] +pub struct Welcome { + pub bind_token: Uuid, + pub email: String, +} + +impl Message for Welcome { + type Result = Result<(), String>; +} + +impl Handler for MailExecutor { + type Result = Result<(), String>; + + fn handle(&mut self, msg: Welcome, _ctx: &mut Self::Context) -> Self::Result { + use lettre::Transport; + let transport = &mut self.transport; + let from = self.config.from.as_str(); + + let html = format!( + r#" + + + + +

Welcome in JIRS!

+

+

+

+ Please copy this code to sign-in single use token field:

{bind_token}
+            

+

+ Notice: This token is single use and will be removed from system once you use it. +

+ + + "#, + bind_token = msg.bind_token, + ); + + let email = lettre_email::Email::builder() + .from(from.clone()) + .to(msg.email.as_str()) + .html(html.as_str()) + .subject("Welcome to JIRS") + .build() + .map_err(|_| "Email is not valid".to_string())?; + + transport + .send(email.into()) + .and_then(|_| Ok(())) + .map_err(|e| format!("Mailer: {}", e)) + } +} diff --git a/jirs-server/src/main.rs b/jirs-server/src/main.rs index a4f59e25..6f86f400 100644 --- a/jirs-server/src/main.rs +++ b/jirs-server/src/main.rs @@ -6,14 +6,15 @@ extern crate diesel; extern crate log; use actix_cors::Cors; -use actix_web::{web, App, HttpServer}; +use actix_web::{App, HttpServer}; pub mod db; pub mod errors; +pub mod mail; pub mod middleware; pub mod models; -pub mod routes; pub mod schema; +pub mod web; pub mod ws; #[actix_rt::main] @@ -21,28 +22,28 @@ async fn main() -> Result<(), String> { dotenv::dotenv().ok(); pretty_env_logger::init(); - let port = std::env::var("JIRS_SERVER_PORT").unwrap_or_else(|_| "3000".to_string()); - let bind = std::env::var("JIRS_SERVER_BIND").unwrap_or_else(|_| "0.0.0.0".to_string()); - let addr = format!("{}:{}", bind, port); + let web_config = web::Configuration::read(); - let db_addr = actix::SyncArbiter::start(4, crate::db::DbExecutor::default); + let db_addr = actix::SyncArbiter::start( + crate::db::Configuration::read().concurrency, + crate::db::DbExecutor::default, + ); + let mail_addr = actix::SyncArbiter::start( + crate::mail::Configuration::read().concurrency, + crate::mail::MailExecutor::default, + ); HttpServer::new(move || { App::new() .wrap(actix_web::middleware::Logger::default()) .wrap(Cors::default()) .data(db_addr.clone()) + .data(mail_addr.clone()) .data(crate::db::build_pool()) .service(crate::ws::index) - .service( - web::scope("/comments") - .service(crate::routes::comments::create) - .service(crate::routes::comments::update) - .service(crate::routes::comments::delete), - ) - .service(web::scope("/currentUser").service(crate::routes::users::current_user)) }) - .bind(addr) + .workers(web_config.concurrency) + .bind(web_config.addr()) .map_err(|e| format!("{}", e))? .run() .await diff --git a/jirs-server/src/routes/comments.rs b/jirs-server/src/routes/comments.rs deleted file mode 100644 index 9980ca96..00000000 --- a/jirs-server/src/routes/comments.rs +++ /dev/null @@ -1,77 +0,0 @@ -use actix::Addr; -use actix_web::web::{Data, Json, Path}; -use actix_web::{delete, post, put, HttpRequest, HttpResponse}; - -use crate::db::comments::{CreateComment, DeleteComment, UpdateComment}; -use crate::db::DbExecutor; -use crate::routes::user_from_request; - -#[post("")] -pub async fn create( - req: HttpRequest, - payload: Json, - db: Data>, -) -> HttpResponse { - let user = match user_from_request(req, &db).await { - Ok(user) => user, - Err(response) => return response, - }; - - let msg = CreateComment { - body: payload.body.clone(), - issue_id: payload.issue_id, - user_id: user.id, - }; - let comment = match db.send(msg).await { - Ok(Ok(comment)) => comment, - _ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(), - }; - - HttpResponse::Ok().json(comment) -} - -#[put("/{id}")] -pub async fn update( - req: HttpRequest, - path: Path, - db: Data>, - payload: Json, -) -> HttpResponse { - let user = match user_from_request(req, &db).await { - Ok(user) => user, - Err(response) => return response, - }; - let comment_id = path.into_inner(); - let body = payload.body.clone(); - - let msg = UpdateComment { - comment_id, - body, - user_id: user.id, - }; - let comment = match db.send(msg).await { - Ok(Ok(comment)) => comment, - Ok(Err(e)) => return e.into_http_response(), - _ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(), - }; - HttpResponse::Ok().json(comment) -} - -#[delete("/{id}")] -pub async fn delete(req: HttpRequest, path: Path, db: Data>) -> HttpResponse { - let user = match user_from_request(req, &db).await { - Ok(user) => user, - Err(response) => return response, - }; - let comment_id = path.into_inner(); - let msg = DeleteComment { - user_id: user.id, - comment_id, - }; - match db.send(msg).await { - Ok(Ok(_)) => (), - Ok(Err(_)) => (), - _ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(), - }; - HttpResponse::NoContent().body("") -} diff --git a/jirs-server/src/routes/mod.rs b/jirs-server/src/routes/mod.rs deleted file mode 100644 index ed3d62ba..00000000 --- a/jirs-server/src/routes/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -use actix::Addr; -use actix_web::web::Data; -use actix_web::{HttpRequest, HttpResponse}; - -use crate::db::authorize_user::AuthorizeUser; -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; -use crate::middleware::authorize::token_from_headers; -use crate::models::User; - -pub mod comments; -pub mod users; - -pub async fn user_from_request( - req: HttpRequest, - db: &Data>, -) -> Result { - let token = match token_from_headers(req.headers()) { - Ok(uuid) => uuid, - _ => return Err(ServiceErrors::Unauthorized.into_http_response()), - }; - match db - .send(AuthorizeUser { - access_token: token, - }) - .await - { - Ok(Ok(user)) => Ok(user), - Ok(Err(e)) => Err(e.into_http_response()), - _ => Err(ServiceErrors::Unauthorized.into_http_response()), - } -} diff --git a/jirs-server/src/routes/users.rs b/jirs-server/src/routes/users.rs deleted file mode 100644 index 6a1791d1..00000000 --- a/jirs-server/src/routes/users.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::db::authorize_user::AuthorizeUser; -use crate::db::DbExecutor; -use crate::middleware::authorize::token_from_headers; -use actix::Addr; -use actix_web::web::Data; -use actix_web::{get, HttpRequest, HttpResponse}; - -#[get("")] -pub async fn current_user(req: HttpRequest, db: Data>) -> HttpResponse { - let token = match token_from_headers(req.headers()) { - Ok(uuid) => uuid, - _ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(), - }; - match db - .send(AuthorizeUser { - access_token: token, - }) - .await - { - Ok(Ok(user)) => HttpResponse::Ok().json(user), - _ => crate::errors::ServiceErrors::Unauthorized.into_http_response(), - } -} diff --git a/jirs-server/src/web/mod.rs b/jirs-server/src/web/mod.rs new file mode 100644 index 00000000..e07a16ef --- /dev/null +++ b/jirs-server/src/web/mod.rs @@ -0,0 +1,79 @@ +use std::fs::*; + +use actix::Addr; +use actix_web::web::Data; +use actix_web::{HttpRequest, HttpResponse}; +use serde::{Deserialize, Serialize}; + +use crate::db::authorize_user::AuthorizeUser; +use crate::db::DbExecutor; +use crate::errors::ServiceErrors; +use crate::middleware::authorize::token_from_headers; +use crate::models::User; + +pub async fn user_from_request( + req: HttpRequest, + db: &Data>, +) -> Result { + let token = match token_from_headers(req.headers()) { + Ok(uuid) => uuid, + _ => return Err(ServiceErrors::Unauthorized.into_http_response()), + }; + match db + .send(AuthorizeUser { + access_token: token, + }) + .await + { + Ok(Ok(user)) => Ok(user), + Ok(Err(e)) => Err(e.into_http_response()), + _ => Err(ServiceErrors::Unauthorized.into_http_response()), + } +} + +#[derive(Serialize, Deserialize)] +pub struct Configuration { + pub concurrency: usize, + pub port: String, + pub bind: String, + pub ssl: bool, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + concurrency: 2, + port: "5000".to_string(), + bind: "0.0.0.0".to_string(), + ssl: false, + } + } +} + +impl Configuration { + pub fn addr(&self) -> String { + format!("{}:{}", self.bind, self.port) + } + + pub fn read() -> Self { + let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); + match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => { + let config = Configuration::default(); + config.write().unwrap_or_else(|e| panic!(e)); + config + } + } + } + + pub fn write(&self) -> Result<(), String> { + let s = toml::to_string(self).map_err(|e| e.to_string())?; + write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; + Ok(()) + } + + pub fn config_file() -> &'static str { + "web.toml" + } +} diff --git a/jirs-server/src/ws/auth.rs b/jirs-server/src/ws/auth.rs index 733dbe9f..1266dcda 100644 --- a/jirs-server/src/ws/auth.rs +++ b/jirs-server/src/ws/auth.rs @@ -1,12 +1,21 @@ +use actix::Addr; +use actix_web::web::Data; + +use jirs_data::WsMsg; + use crate::db::tokens::CreateBindToken; use crate::db::users::FindUser; use crate::db::DbExecutor; +use crate::mail::welcome::Welcome; +use crate::mail::MailExecutor; use crate::ws::WsResult; -use actix::Addr; -use actix_web::web::Data; -use jirs_data::WsMsg; -pub async fn authenticate(db: &Data>, name: String, email: String) -> WsResult { +pub async fn authenticate( + db: &Data>, + mail: &Data>, + name: String, + email: String, +) -> WsResult { // TODO check attempt number, allow only 5 times per day let user = match db.send(FindUser { name, email }).await { Ok(Ok(user)) => user, @@ -19,7 +28,7 @@ pub async fn authenticate(db: &Data>, name: String, email: Stri return Ok(None); } }; - let _token = match db.send(CreateBindToken { user_id: user.id }).await { + let token = match db.send(CreateBindToken { user_id: user.id }).await { Ok(Ok(token)) => token, Ok(Err(e)) => { error!("{:?}", e); @@ -30,6 +39,24 @@ pub async fn authenticate(db: &Data>, name: String, email: Stri return Ok(None); } }; - // TODO send email somehow + if let Some(bind_token) = token.bind_token.as_ref().cloned() { + match mail + .send(Welcome { + bind_token, + email: user.email.clone(), + }) + .await + { + Ok(Ok(_)) => (), + Ok(Err(e)) => { + error!("{}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } + } + } Ok(Some(WsMsg::AuthenticateSuccess)) } diff --git a/jirs-server/src/ws/issues.rs b/jirs-server/src/ws/issues.rs index 5554474e..967664a4 100644 --- a/jirs-server/src/ws/issues.rs +++ b/jirs-server/src/ws/issues.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use actix::Addr; use actix_web::web::Data; -use jirs_data::WsMsg; +use jirs_data::{IssueFieldId, PayloadVariant, WsMsg}; use crate::db::issue_assignees::LoadAssignees; use crate::db::issues::{LoadProjectIssues, UpdateIssue}; @@ -14,28 +14,51 @@ pub async fn update_issue( db: &Data>, user: &Option, issue_id: i32, - payload: jirs_data::UpdateIssuePayload, + issue_field_id: IssueFieldId, + payload: PayloadVariant, ) -> WsResult { current_user(user)?; - let mut issue: jirs_data::Issue = match db - .send(UpdateIssue { - issue_id, - title: Some(payload.title), - issue_type: Some(payload.issue_type), - status: Some(payload.status), - priority: Some(payload.priority), - list_position: Some(payload.list_position), - description: payload.description, - description_text: payload.description_text, - estimate: payload.estimate, - time_spent: payload.time_spent, - time_remaining: payload.time_remaining, - project_id: Some(payload.project_id), - user_ids: Some(payload.user_ids), - reporter_id: Some(payload.reporter_id), - }) - .await - { + + let mut msg = UpdateIssue::default(); + msg.issue_id = issue_id; + match (issue_field_id, payload) { + (IssueFieldId::Type, PayloadVariant::IssueType(t)) => { + msg.issue_type = Some(t); + } + (IssueFieldId::Title, PayloadVariant::String(s)) => { + msg.title = Some(s); + } + (IssueFieldId::Description, PayloadVariant::String(s)) => { + msg.description = Some(s); + } + (IssueFieldId::Status, PayloadVariant::IssueStatus(s)) => { + msg.status = Some(s); + } + (IssueFieldId::ListPosition, PayloadVariant::I32(i)) => { + msg.list_position = Some(i); + } + (IssueFieldId::Assignees, PayloadVariant::VecI32(v)) => { + msg.user_ids = Some(v); + } + (IssueFieldId::Reporter, PayloadVariant::I32(i)) => { + msg.reporter_id = Some(i); + } + (IssueFieldId::Priority, PayloadVariant::IssuePriority(p)) => { + msg.priority = Some(p); + } + (IssueFieldId::Estimate, PayloadVariant::OptionI32(o)) => { + msg.estimate = o; + } + (IssueFieldId::TimeSpend, PayloadVariant::OptionI32(o)) => { + msg.time_spent = o; + } + (IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => { + msg.time_remaining = o; + } + _ => (), + }; + + let mut issue: jirs_data::Issue = match db.send(msg).await { Ok(Ok(issue)) => issue.into(), _ => return Ok(None), }; diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index 61816edd..f4e70d7f 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -8,6 +8,7 @@ use jirs_data::WsMsg; use crate::db::authorize_user::AuthorizeUser; use crate::db::tokens::FindBindToken; use crate::db::DbExecutor; +use crate::mail::MailExecutor; pub mod auth; pub mod comments; @@ -30,6 +31,7 @@ trait WsMessageSender { struct WebSocketActor { db: Data>, + mail: Data>, current_user: Option, } @@ -51,81 +53,79 @@ impl WebSocketActor { info!("incoming message: {:?}", msg); } - let msg = match msg { - WsMsg::Ping => Some(WsMsg::Pong), - WsMsg::Pong => Some(WsMsg::Ping), + let msg = + match msg { + WsMsg::Ping => Some(WsMsg::Pong), + WsMsg::Pong => Some(WsMsg::Ping), - // Issues - WsMsg::IssueUpdateRequest(id, payload) => block_on(issues::update_issue( - &self.db, - &self.current_user, - id, - payload, - ))?, - WsMsg::IssueCreateRequest(payload) => { - block_on(issues::add_issue(&self.db, &self.current_user, payload))? - } - WsMsg::IssueDeleteRequest(id) => { - block_on(issues::delete_issue(&self.db, &self.current_user, id))? - } - WsMsg::ProjectIssuesRequest => { - block_on(issues::load_issues(&self.db, &self.current_user))? - } + // Issues + WsMsg::IssueUpdateRequest(id, field_id, payload) => block_on( + issues::update_issue(&self.db, &self.current_user, id, field_id, payload), + )?, + WsMsg::IssueCreateRequest(payload) => { + block_on(issues::add_issue(&self.db, &self.current_user, payload))? + } + WsMsg::IssueDeleteRequest(id) => { + block_on(issues::delete_issue(&self.db, &self.current_user, id))? + } + WsMsg::ProjectIssuesRequest => { + block_on(issues::load_issues(&self.db, &self.current_user))? + } - // projects - WsMsg::ProjectRequest => { - block_on(projects::current_project(&self.db, &self.current_user))? - } + // projects + WsMsg::ProjectRequest => { + block_on(projects::current_project(&self.db, &self.current_user))? + } - WsMsg::ProjectUpdateRequest(payload) => block_on(projects::update_project( - &self.db, - &self.current_user, - payload, - ))?, + WsMsg::ProjectUpdateRequest(payload) => block_on(projects::update_project( + &self.db, + &self.current_user, + payload, + ))?, - // auth - WsMsg::AuthorizeRequest(uuid) => block_on(self.check_auth_token(uuid))?, - WsMsg::BindTokenCheck(uuid) => block_on(self.check_bind_token(uuid))?, - WsMsg::AuthenticateRequest(email, name) => { - block_on(auth::authenticate(&self.db, name, email))? - } + // auth + WsMsg::AuthorizeRequest(uuid) => block_on(self.check_auth_token(uuid))?, + WsMsg::BindTokenCheck(uuid) => block_on(self.check_bind_token(uuid))?, + WsMsg::AuthenticateRequest(email, name) => { + block_on(auth::authenticate(&self.db, &self.mail, name, email))? + } - // users - WsMsg::ProjectUsersRequest => { - block_on(users::load_project_users(&self.db, &self.current_user))? - } + // users + WsMsg::ProjectUsersRequest => { + block_on(users::load_project_users(&self.db, &self.current_user))? + } - // comments - WsMsg::IssueCommentsRequest(issue_id) => block_on(comments::load_issues( - &self.db, - &self.current_user, - issue_id, - ))?, + // comments + WsMsg::IssueCommentsRequest(issue_id) => block_on(comments::load_issues( + &self.db, + &self.current_user, + issue_id, + ))?, - WsMsg::CreateComment(payload) => block_on(comments::create_comment( - &self.db, - &self.current_user, - payload, - ))?, + WsMsg::CreateComment(payload) => block_on(comments::create_comment( + &self.db, + &self.current_user, + payload, + ))?, - WsMsg::UpdateComment(payload) => block_on(comments::update_comment( - &self.db, - &self.current_user, - payload, - ))?, + WsMsg::UpdateComment(payload) => block_on(comments::update_comment( + &self.db, + &self.current_user, + payload, + ))?, - WsMsg::CommentDeleteRequest(comment_id) => block_on(comments::delete_comment( - &self.db, - &self.current_user, - comment_id, - ))?, + WsMsg::CommentDeleteRequest(comment_id) => block_on(comments::delete_comment( + &self.db, + &self.current_user, + comment_id, + ))?, - // else fail - _ => { - error!("No handle for {:?} specified", msg); - None - } - }; + // else fail + _ => { + error!("No handle for {:?} specified", msg); + None + } + }; if msg.is_some() && msg != Some(WsMsg::Pong) { info!("sending message {:?}", msg); } @@ -193,10 +193,12 @@ pub async fn index( req: HttpRequest, stream: web::Payload, db: Data>, + mail: Data>, ) -> Result { ws::start( WebSocketActor { db, + mail, current_user: None, }, &req,