Reduce update issue payload. Create config files

This commit is contained in:
Adrian Woźniak 2020-04-17 14:10:05 +02:00
parent fbee0e70d6
commit 2f870a5a0a
31 changed files with 1375 additions and 606 deletions

6
.env
View File

@ -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

3
.gitignore vendored
View File

@ -1 +1,4 @@
/target
mail.toml
web.toml
db.toml

468
Cargo.lock generated
View File

@ -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"

View File

@ -23,90 +23,85 @@ pub type AvatarFilterActive = bool;
pub type AppType = App<Msg, Model, Node<Msg>>;
#[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();

View File

@ -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<Msg>) {
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<Msg> {
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<Msg> {
.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<Msg> {
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<Msg> {
.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<Msg> {
.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,8 +235,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build()
.into_node();
let select_priority =
StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Priority))
let select_priority = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Priority))
.name("priority")
.normal()
.text_filter(modal.priority_state.text_filter.as_str())

View File

@ -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<Msg>) {
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<Msg>) {
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<Msg>) {
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::<i32>().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::<i32>().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<Msg>) {
}
}
// 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::<i32>() {
Msg::InputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
value,
) => match value.parse::<i32>() {
Ok(n) if !value.is_empty() => {
modal.payload.estimate = Some(n);
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
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, modal.payload.clone()));
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<Msg>) {
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<Msg>) {
orders
.skip()
.send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
true,
)));
}
@ -256,8 +327,9 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let issue_type_select =
StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::IssueType))
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())
@ -305,12 +377,15 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.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))
let description = StyledEditor::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Description,
)))
.text(description_text)
.mode(description_editor_mode.clone())
.update_on(Ev::Change)
@ -338,7 +413,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
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<Node<Msg>> {
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<Node<Msg>> {
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<N
.add_class("editButton")
.on_click(mouse_ev(Ev::Click, move |_| {
Msg::ModalChanged(FieldChange::EditComment(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
comment_id,
))
}))
@ -486,7 +563,9 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
..
} = modal;
let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Status))
let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Status,
)))
.name("status")
.opened(status_state.opened)
.normal()
@ -507,7 +586,9 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Assignees))
let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Assignees,
)))
.name("assignees")
.opened(assignees_state.opened)
.empty()
@ -536,7 +617,9 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Reporter))
let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Reporter,
)))
.name("reporter")
.opened(reporter_state.opened)
.empty()
@ -564,7 +647,9 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Priority))
let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Priority,
)))
.name("priority")
.opened(priority_state.opened)
.empty()
@ -584,7 +669,9 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate))
let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Estimate,
)))
.valid(true)
.value(
payload

View File

@ -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<Msg> {
let _issue = match find_issue(model, issue_id) {
@ -26,7 +26,9 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
let tracking = tracking_widget(model, edit_issue_modal);
let time_spent = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::TimeSpend))
let time_spent = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::TimeSpend,
)))
.value(
edit_issue_modal
.payload
@ -43,9 +45,9 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
.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

View File

@ -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,
)),
}
}

View File

@ -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<Msg>) {
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

View File

@ -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<Msg>) {
if model.user.is_none() {
@ -45,24 +45,24 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
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<Msg> {
.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<Msg> {
.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,9 +112,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build()
.into_node();
let description = StyledEditor::build(FieldId::ProjectSettings(
ProjectSettingsFieldId::Description,
))
let description = StyledEditor::build(FieldId::ProjectSettings(ProjectFieldId::Description))
.text(
page.payload
.description
@ -133,7 +131,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.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)

View File

@ -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;

View File

@ -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<ProjectCategory>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum PayloadVariant {
OptionI32(Option<i32>),
VecI32(Vec<i32>),
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),

View File

@ -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"

View File

@ -23,7 +23,7 @@ impl Handler<AuthorizeUser> for DbExecutor {
use crate::schema::users::dsl::{id, users};
let conn = &self
.0
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let token = tokens

View File

@ -22,7 +22,7 @@ impl Handler<LoadIssueComments> for DbExecutor {
use crate::schema::comments::dsl::*;
let conn = &self
.0
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let rows: Vec<Comment> = comments
@ -59,7 +59,7 @@ impl Handler<CreateComment> 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<UpdateComment> 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<DeleteComment> for DbExecutor {
use crate::schema::comments::dsl::*;
let conn = &self
.0
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
diesel::delete(

View File

@ -22,7 +22,7 @@ impl Handler<LoadAssignees> for DbExecutor {
use crate::schema::issue_assignees::dsl::*;
let conn = &self
.0
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
issue_assignees

View File

@ -27,7 +27,7 @@ impl Handler<LoadIssue> 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<LoadProjectIssues> 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<LoadProjectIssues> for DbExecutor {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Default)]
pub struct UpdateIssue {
pub issue_id: i32,
pub title: Option<String>,
@ -97,7 +97,7 @@ impl Handler<UpdateIssue> 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<DeleteIssue> for DbExecutor {
use crate::schema::issues::dsl::issues;
let conn = &self
.0
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
@ -229,7 +229,7 @@ impl Handler<CreateIssue> for DbExecutor {
use crate::schema::issues::dsl::{issues, status};
let conn = &self
.0
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;

View File

@ -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<ConnectionManager<dev::VerboseConnection>>;
#[cfg(not(debug_assertions))]
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
pub struct DbExecutor(pub DbPool);
pub struct DbExecutor {
pub pool: DbPool,
pub config: Configuration,
}
impl Actor for DbExecutor {
type Context = SyncContext<Self>;
@ -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"
}
}

View File

@ -23,7 +23,7 @@ impl Handler<LoadCurrentProject> 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<UpdateProject> 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)?;

View File

@ -24,7 +24,7 @@ impl Handler<FindBindToken> 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<CreateBindToken> 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)?;

View File

@ -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<FindUser> 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<LoadProjectUsers> for DbExecutor {
use crate::schema::users::dsl::*;
let conn = &self
.0
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let rows: Vec<User> = users
@ -82,7 +83,7 @@ impl Handler<LoadIssueAssignees> for DbExecutor {
use crate::schema::users::dsl::*;
let conn = &self
.0
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let rows: Vec<(User, IssueAssignee)> = users

View File

@ -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<Self>;
}
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"
}
}

View File

@ -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<Welcome> 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#"
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<h1>Welcome in JIRS!</h1>
<p>
</p>
<p>
Please copy this code to sign-in single use token field: <pre><code>{bind_token}</code</pre>
</p>
<p>
Notice: This token is single use and will be removed from system once you use it.
</p>
</body>
</html>
"#,
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))
}
}

View File

@ -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

View File

@ -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<jirs_data::CreateCommentPayload>,
db: Data<Addr<DbExecutor>>,
) -> 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<i32>,
db: Data<Addr<DbExecutor>>,
payload: Json<jirs_data::UpdateCommentPayload>,
) -> 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<i32>, db: Data<Addr<DbExecutor>>) -> 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("")
}

View File

@ -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<Addr<DbExecutor>>,
) -> Result<User, HttpResponse> {
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()),
}
}

View File

@ -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<Addr<DbExecutor>>) -> 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(),
}
}

View File

@ -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<Addr<DbExecutor>>,
) -> Result<User, HttpResponse> {
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"
}
}

View File

@ -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<Addr<DbExecutor>>, name: String, email: String) -> WsResult {
pub async fn authenticate(
db: &Data<Addr<DbExecutor>>,
mail: &Data<Addr<MailExecutor>>,
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<Addr<DbExecutor>>, 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<Addr<DbExecutor>>, 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))
}

View File

@ -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<Addr<DbExecutor>>,
user: &Option<jirs_data::User>,
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),
};

View File

@ -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<Addr<DbExecutor>>,
mail: Data<Addr<MailExecutor>>,
current_user: Option<jirs_data::User>,
}
@ -51,17 +53,15 @@ impl WebSocketActor {
info!("incoming message: {:?}", msg);
}
let msg = match msg {
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::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))?
}
@ -87,7 +87,7 @@ impl WebSocketActor {
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))?
block_on(auth::authenticate(&self.db, &self.mail, name, email))?
}
// users
@ -193,10 +193,12 @@ pub async fn index(
req: HttpRequest,
stream: web::Payload,
db: Data<Addr<DbExecutor>>,
mail: Data<Addr<MailExecutor>>,
) -> Result<HttpResponse, Error> {
ws::start(
WebSocketActor {
db,
mail,
current_user: None,
},
&req,