Huge optimizations, code organization and refactoring
This commit is contained in:
parent
5a7ddb03e1
commit
01ce1794cd
189
Cargo.lock
generated
189
Cargo.lock
generated
@ -10,7 +10,7 @@ dependencies = [
|
||||
"actix-rt",
|
||||
"actix_derive",
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"crossbeam-channel",
|
||||
"derive_more",
|
||||
"futures 0.3.8",
|
||||
@ -34,7 +34,7 @@ dependencies = [
|
||||
"actix-rt",
|
||||
"actix_derive",
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"crossbeam-channel",
|
||||
"derive_more",
|
||||
"futures-channel",
|
||||
@ -57,7 +57,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"log",
|
||||
@ -72,7 +72,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"log",
|
||||
@ -135,14 +135,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "actix-files"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d031468a7859f71674e5531bd05137e0ea5de05ec9a917314330b88c582e2e0a"
|
||||
checksum = "c51e8a9146c12fce92a6e4c24b8c4d9b05268130bfd8d61bc587e822c32ce689"
|
||||
dependencies = [
|
||||
"actix-service",
|
||||
"actix-web",
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@ -167,7 +167,7 @@ dependencies = [
|
||||
"actix-utils 1.0.6",
|
||||
"base64 0.11.0",
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"copyless",
|
||||
"derive_more",
|
||||
@ -212,8 +212,8 @@ dependencies = [
|
||||
"base64 0.13.0",
|
||||
"bitflags",
|
||||
"brotli2",
|
||||
"bytes 0.5.6",
|
||||
"cookie 0.14.3",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"copyless",
|
||||
"derive_more",
|
||||
"either",
|
||||
@ -263,7 +263,7 @@ dependencies = [
|
||||
"actix-service",
|
||||
"actix-utils 2.0.0",
|
||||
"actix-web",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"futures-util",
|
||||
"httparse",
|
||||
@ -381,7 +381,7 @@ dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"either",
|
||||
"futures 0.3.8",
|
||||
"log",
|
||||
@ -399,7 +399,7 @@ dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"either",
|
||||
"futures-channel",
|
||||
"futures-sink",
|
||||
@ -428,7 +428,7 @@ dependencies = [
|
||||
"actix-utils 2.0.0",
|
||||
"actix-web-codegen",
|
||||
"awc",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
@ -458,7 +458,7 @@ dependencies = [
|
||||
"actix-codec 0.3.0",
|
||||
"actix-http 2.2.0",
|
||||
"actix-web",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"pin-project 0.4.27",
|
||||
@ -488,9 +488,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.14.0"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
|
||||
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@ -510,6 +510,30 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "amazon-actor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix 0.10.0",
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-web-actors",
|
||||
"bytes",
|
||||
"env_logger",
|
||||
"futures 0.3.8",
|
||||
"jirs-config",
|
||||
"libc",
|
||||
"log",
|
||||
"openssl-sys",
|
||||
"pretty_env_logger",
|
||||
"rusoto_core",
|
||||
"rusoto_s3",
|
||||
"rusoto_signature",
|
||||
"serde",
|
||||
"tokio",
|
||||
"uuid 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.11.0"
|
||||
@ -582,7 +606,7 @@ dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"base64 0.13.0",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"cfg-if 1.0.0",
|
||||
"derive_more",
|
||||
"futures-core",
|
||||
@ -742,11 +766,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "buf-min"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "881e704e61d0fb41d7c6c9ae2bd790eb8c13dc974ae102fb98c788b4fdea4349"
|
||||
checksum = "fa17aa1cf56bdd6bb30518767d00e58019d326f3f05d8c3e0730b549d332ea83"
|
||||
dependencies = [
|
||||
"bytes 0.6.0",
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -779,19 +803,13 @@ version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16"
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -899,16 +917,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c60ef6d0bbf56ad2674249b6bb74f2c6aeb98b98dd57b5d3e37cace33011d69"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time 0.2.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.14.3"
|
||||
@ -1146,9 +1154,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b"
|
||||
checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
@ -1324,7 +1332,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix 0.10.0",
|
||||
"actix-files",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"env_logger",
|
||||
"futures 0.3.8",
|
||||
"jirs-config",
|
||||
@ -1653,7 +1661,7 @@ version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@ -1752,7 +1760,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
@ -1763,7 +1771,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
@ -1794,7 +1802,7 @@ version = "0.13.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@ -1818,7 +1826,7 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
@ -1916,9 +1924,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
|
||||
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||
|
||||
[[package]]
|
||||
name = "jirs-cli"
|
||||
@ -1973,6 +1981,7 @@ dependencies = [
|
||||
"actix-service",
|
||||
"actix-web",
|
||||
"actix-web-actors",
|
||||
"amazon-actor",
|
||||
"async-trait",
|
||||
"bigdecimal",
|
||||
"bincode",
|
||||
@ -2014,7 +2023,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
"comrak",
|
||||
"futures 0.1.30",
|
||||
"jirs-data",
|
||||
"js-sys",
|
||||
@ -2320,9 +2328,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f"
|
||||
checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
@ -2469,9 +2477,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.31"
|
||||
version = "0.10.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d008f51b1acffa0d3450a68606e6a51c123012edaacb0f4e1426bd978869187"
|
||||
checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
@ -2498,9 +2506,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.59"
|
||||
version = "0.9.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de52d8eabd217311538a39bba130d7dea1f1e118010fee7a033d966845e7d5fe"
|
||||
checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"cc",
|
||||
@ -2734,9 +2742,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.7.2"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca36dea94d187597e104a5c8e4b07576a8a45aa5db48a65e12940d3eb7461f55"
|
||||
checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"getopts",
|
||||
@ -3030,7 +3038,7 @@ checksum = "e977941ee0658df96fca7291ecc6fc9a754600b21ad84b959eb1dbbc9d5abcc7"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.12.3",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"crc32fast",
|
||||
"futures 0.3.8",
|
||||
"http",
|
||||
@ -3077,7 +3085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1146e37a7c1df56471ea67825fe09bbbd37984b5f6e201d8b2e0be4ee15643d8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"futures 0.3.8",
|
||||
"rusoto_core",
|
||||
"xml-rs",
|
||||
@ -3090,7 +3098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a740a88dde8ded81b6f2cff9cd5e054a5a2e38a38397260f7acdd2c85d17dd"
|
||||
dependencies = [
|
||||
"base64 0.12.3",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"futures 0.3.8",
|
||||
"hex",
|
||||
"hmac",
|
||||
@ -3212,12 +3220,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "seed"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "882f4569a394bbb2f15f2fc410e0fbcef178fe24fc2d91599607a598443c6df8"
|
||||
checksum = "3b599be9cc57456f4b7fc99b8abfb154d4819f7b6c147e80be5580663dad4536"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"cookie 0.13.3",
|
||||
"cookie",
|
||||
"dbg",
|
||||
"enclose",
|
||||
"futures 0.3.8",
|
||||
@ -3273,9 +3281,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.60"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779"
|
||||
checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -3395,10 +3403,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "standback"
|
||||
version = "0.2.13"
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf906c8b8fc3f6ecd1046e01da1d8ddec83e48c8b08b84dcc02b585a6bedf5a8"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "standback"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0"
|
||||
dependencies = [
|
||||
"version_check 0.9.2",
|
||||
]
|
||||
@ -3466,9 +3480,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.55"
|
||||
version = "1.0.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a"
|
||||
checksum = "a9802ddde94170d186eeee5005b798d9c159fa970403f1be19976d0cfb939b72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3555,18 +3569,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e"
|
||||
checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56"
|
||||
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3661,7 +3675,7 @@ version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"iovec",
|
||||
@ -3705,7 +3719,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"log",
|
||||
@ -3719,7 +3733,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
@ -4002,9 +4016,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "v_escape"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccca9e73c678b882900cbaec16dae4d3662ace5a17774ac45af04e0f3988fafa"
|
||||
checksum = "f3e0ab5fab1db278a9413d2ea794cb66f471f898c5b020c3c394f6447625d9d4"
|
||||
dependencies = [
|
||||
"buf-min",
|
||||
"v_escape_derive",
|
||||
@ -4024,9 +4038,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "v_htmlescape"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db00c903248abee8499af60bf20d242e7882335bbbffd2614915184cbb207402"
|
||||
checksum = "1f9a8af610ad6f7fc9989c9d2590d9764bc61f294884e9ee93baa58795174572"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"v_escape",
|
||||
@ -4192,26 +4206,21 @@ dependencies = [
|
||||
"actix-service",
|
||||
"actix-web",
|
||||
"actix-web-actors",
|
||||
"amazon-actor",
|
||||
"bincode",
|
||||
"bytes 0.5.6",
|
||||
"bytes",
|
||||
"database-actor",
|
||||
"env_logger",
|
||||
"filesystem-actor",
|
||||
"flate2",
|
||||
"futures 0.3.8",
|
||||
"jirs-config",
|
||||
"jirs-data",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"mail-actor",
|
||||
"openssl-sys",
|
||||
"pretty_env_logger",
|
||||
"rusoto_core",
|
||||
"rusoto_s3",
|
||||
"rusoto_signature",
|
||||
"serde",
|
||||
"syntect",
|
||||
"tokio",
|
||||
"toml",
|
||||
"uuid 0.8.1",
|
||||
@ -4236,10 +4245,12 @@ dependencies = [
|
||||
"actix-web",
|
||||
"actix-web-actors",
|
||||
"bincode",
|
||||
"comrak",
|
||||
"database-actor",
|
||||
"env_logger",
|
||||
"flate2",
|
||||
"futures 0.3.8",
|
||||
"highlight-actor",
|
||||
"jirs-config",
|
||||
"jirs-data",
|
||||
"lazy_static",
|
||||
@ -4248,6 +4259,7 @@ dependencies = [
|
||||
"mail-actor",
|
||||
"openssl-sys",
|
||||
"pretty_env_logger",
|
||||
"pulldown-cmark",
|
||||
"serde",
|
||||
"syntect",
|
||||
"toml",
|
||||
@ -4263,6 +4275,7 @@ dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"libc",
|
||||
"memory_units",
|
||||
"spin",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
|
@ -12,7 +12,6 @@
|
||||
members = [
|
||||
"./jirs-cli",
|
||||
"./jirs-server",
|
||||
"./jirs-client",
|
||||
"./jirs-css",
|
||||
"./shared/jirs-config",
|
||||
"./shared/jirs-data",
|
||||
@ -22,5 +21,8 @@ members = [
|
||||
"./actors/web-actor",
|
||||
"./actors/websocket-actor",
|
||||
"./actors/mail-actor",
|
||||
"./actors/filesystem-actor"
|
||||
"./actors/amazon-actor",
|
||||
"./actors/filesystem-actor",
|
||||
# Client
|
||||
"./jirs-client"
|
||||
]
|
||||
|
@ -42,6 +42,14 @@ https://git.sr.ht/~tsumanu/jirs
|
||||
* Add personal settings to choose MDE (Markdown Editor) or RTE
|
||||
* Add issues and filters
|
||||
|
||||
##### Version 1.1.1
|
||||
|
||||
* Refactor actors
|
||||
* Extract code highlight to server actor
|
||||
* Handle upload avatar with stream
|
||||
* Move config to `./config` directory
|
||||
* Fix S3 upload with upgraded version of `rusoto`
|
||||
|
||||
##### Work Progress
|
||||
|
||||
* [X] Add Epic
|
||||
|
51
actors/amazon-actor/Cargo.toml
Normal file
51
actors/amazon-actor/Cargo.toml
Normal file
@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "amazon-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "amazon_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = "*"
|
||||
|
||||
actix = { version = "0.10.0" }
|
||||
actix-service = { version = "*" }
|
||||
actix-rt = "1"
|
||||
actix-web-actors = "*"
|
||||
|
||||
bytes = { version = "0.5.6" }
|
||||
|
||||
futures = { version = "0.3.8" }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["mail", "web", "local-storage"]
|
||||
|
||||
# Amazon S3
|
||||
[dependencies.rusoto_s3]
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.rusoto_core]
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.rusoto_signature]
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "0.2.23"
|
||||
features = ["tcp", "time", "rt-core", "fs"]
|
87
actors/amazon-actor/src/lib.rs
Normal file
87
actors/amazon-actor/src/lib.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use {
|
||||
actix,
|
||||
rusoto_s3::{PutObjectRequest, S3Client, S3},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AmazonError {
|
||||
UploadFailed,
|
||||
}
|
||||
|
||||
pub struct AmazonExecutor;
|
||||
|
||||
impl Default for AmazonExecutor {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl actix::Actor for AmazonExecutor {
|
||||
type Context = actix::SyncContext<Self>;
|
||||
}
|
||||
|
||||
#[derive(actix::Message)]
|
||||
#[rtype(result = "Result<String, AmazonError>")]
|
||||
pub struct S3PutObject {
|
||||
pub source: tokio::sync::broadcast::Receiver<bytes::Bytes>,
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
impl actix::Handler<S3PutObject> for AmazonExecutor {
|
||||
type Result = Result<String, AmazonError>;
|
||||
|
||||
fn handle(&mut self, msg: S3PutObject, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let S3PutObject {
|
||||
mut source,
|
||||
file_name,
|
||||
} = msg;
|
||||
jirs_config::amazon::config().set_variables();
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("Failed to start amazon agent")
|
||||
.block_on(async {
|
||||
let s3 = jirs_config::amazon::config();
|
||||
log::debug!("{:?}", s3);
|
||||
|
||||
// TODO: Unable to upload as stream because there is no size_hint
|
||||
// use futures::stream::*;
|
||||
// let stream = source
|
||||
// .into_stream()
|
||||
// .map_err(|_e| std::io::Error::from_raw_os_error(1));
|
||||
|
||||
let mut v: Vec<u8> = vec![];
|
||||
use bytes::Buf;
|
||||
while let Ok(b) = source.recv().await {
|
||||
v.extend_from_slice(b.bytes())
|
||||
}
|
||||
|
||||
let client = S3Client::new(s3.region());
|
||||
let put_object = PutObjectRequest {
|
||||
bucket: s3.bucket.clone(),
|
||||
key: file_name.clone(),
|
||||
// body: Some(rusoto_signature::ByteStream::new(stream)),
|
||||
body: Some(v.into()),
|
||||
..Default::default()
|
||||
};
|
||||
let id = match client.put_object(put_object).await {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
return Err(AmazonError::UploadFailed);
|
||||
}
|
||||
};
|
||||
log::debug!("{:?}", id);
|
||||
Ok(aws_s3_url(file_name.as_str()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn aws_s3_url(key: &str) -> String {
|
||||
let config = jirs_config::amazon::config();
|
||||
format!(
|
||||
"https://{bucket}.s3.{region}.amazonaws.com/{key}",
|
||||
bucket = config.bucket,
|
||||
region = config.region_name,
|
||||
key = key
|
||||
)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use jirs_data::HighlightedCode;
|
||||
use {
|
||||
actix::{Actor, Handler, SyncContext},
|
||||
std::sync::Arc,
|
||||
@ -45,17 +46,74 @@ impl Actor for HighlightActor {
|
||||
}
|
||||
|
||||
#[derive(actix::Message)]
|
||||
#[rtype(result = "Result<Vec<u8>, HighlightError>")]
|
||||
#[rtype(result = "Result<HighlightedCode, HighlightError>")]
|
||||
pub struct HighlightCode {
|
||||
pub code: String,
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
impl Handler<HighlightCode> for HighlightActor {
|
||||
type Result = Result<Vec<u8>, HighlightError>;
|
||||
type Result = Result<HighlightedCode, HighlightError>;
|
||||
|
||||
fn handle(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let res = hi(&msg.code, &msg.lang)?;
|
||||
bincode::serialize(&res).map_err(|_| HighlightError::ResultUnserializable)
|
||||
let res: Vec<(Style, &str)> = hi(&msg.code, &msg.lang)?;
|
||||
|
||||
Ok(HighlightedCode {
|
||||
parts: res
|
||||
.into_iter()
|
||||
.map(|(style, part)| {
|
||||
(
|
||||
jirs_data::Style {
|
||||
foreground: jirs_data::Color {
|
||||
r: style.foreground.r,
|
||||
g: style.foreground.g,
|
||||
b: style.foreground.b,
|
||||
a: style.foreground.a,
|
||||
},
|
||||
background: jirs_data::Color {
|
||||
r: style.background.r,
|
||||
g: style.background.g,
|
||||
b: style.background.b,
|
||||
a: style.background.a,
|
||||
},
|
||||
font_style: style.font_style.bits(),
|
||||
},
|
||||
part.to_string(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(actix::Message)]
|
||||
#[rtype(result = "Result<String, HighlightError>")]
|
||||
pub struct TextHighlightCode {
|
||||
pub code: String,
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
impl Handler<TextHighlightCode> for HighlightActor {
|
||||
type Result = Result<String, HighlightError>;
|
||||
|
||||
fn handle(&mut self, msg: TextHighlightCode, ctx: &mut Self::Context) -> Self::Result {
|
||||
let v = self.handle(
|
||||
HighlightCode {
|
||||
lang: msg.lang,
|
||||
code: msg.code,
|
||||
},
|
||||
ctx,
|
||||
)?;
|
||||
Ok(v.parts
|
||||
.into_iter()
|
||||
.fold(String::new(), |mem, (style, text)| {
|
||||
format!(
|
||||
"{mem}<span style=\"color:rgba({fr},{fg},{fb},{fa});background:rgba({br},{bg},{bb},{ba})\">{txt}</span>",
|
||||
mem = mem,
|
||||
fr = style.foreground.r, fg = style.foreground.g, fb = style.foreground.b, fa = style.foreground.a,
|
||||
br = style.background.r, bg = style.background.g, bb = style.background.b, ba = style.background.a,
|
||||
txt = text
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ path = "./src/lib.rs"
|
||||
|
||||
[features]
|
||||
local-storage = ["filesystem-actor"]
|
||||
aws-s3 = ["rusoto_s3", "rusoto_core"]
|
||||
aws-s3 = ["amazon-actor"]
|
||||
default = ["local-storage", "aws-s3"]
|
||||
|
||||
[dependencies]
|
||||
@ -36,10 +36,6 @@ futures = { version = "0.3.8" }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
|
||||
flate2 = { version = "*" }
|
||||
syntect = { version = "*" }
|
||||
lazy_static = { version = "*" }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
@ -67,18 +63,9 @@ path = "../websocket-actor"
|
||||
path = "../filesystem-actor"
|
||||
optional = true
|
||||
|
||||
# Amazon S3
|
||||
[dependencies.rusoto_s3]
|
||||
[dependencies.amazon-actor]
|
||||
path = "../amazon-actor"
|
||||
optional = true
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.rusoto_core]
|
||||
optional = true
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.rusoto_signature]
|
||||
optional = true
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "0.2.23"
|
||||
|
@ -21,6 +21,7 @@ pub async fn upload(
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
ws: Data<Addr<WsServer>>,
|
||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let mut user_id: Option<UserId> = None;
|
||||
let mut avatar_url: Option<String> = None;
|
||||
@ -45,6 +46,7 @@ pub async fn upload(
|
||||
field,
|
||||
disposition,
|
||||
fs.clone(),
|
||||
amazon.clone(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
@ -1,51 +1,40 @@
|
||||
#[cfg(feature = "local-storage")]
|
||||
use filesystem_actor::FileSystemExecutor;
|
||||
use {
|
||||
actix::Addr,
|
||||
actix_multipart::Field,
|
||||
actix_web::{http::header::ContentDisposition, web::Data, Error},
|
||||
futures::{StreamExt, TryStreamExt},
|
||||
futures::StreamExt,
|
||||
jirs_data::UserId,
|
||||
rusoto_core::ByteStream,
|
||||
tokio::sync::broadcast::{Receiver, Sender},
|
||||
};
|
||||
#[cfg(feature = "aws-s3")]
|
||||
use {
|
||||
jirs_config::web::AmazonS3Storage,
|
||||
rusoto_s3::{PutObjectRequest, S3Client, S3},
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "local-storage", feature = "aws-s3"))]
|
||||
pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: Field,
|
||||
disposition: ContentDisposition,
|
||||
fs: Data<Addr<FileSystemExecutor>>,
|
||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = disposition.get_filename().unwrap();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(4);
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
let fs_fut = tokio::task::spawn(local_storage_write(
|
||||
system_file_name.clone(),
|
||||
fs.clone(),
|
||||
user_id,
|
||||
sender.subscribe(),
|
||||
));
|
||||
let fs_fut = local_storage_write(system_file_name.clone(), fs, user_id, sender.subscribe());
|
||||
let aws_fut = aws_s3(system_file_name, amazon, receiver);
|
||||
let read_fut = read_form_data(&mut field, sender);
|
||||
|
||||
// Upload to AWS S3
|
||||
let aws_fut = tokio::task::spawn(aws_s3(system_file_name, receiver));
|
||||
|
||||
read_form_data(&mut field, sender).await;
|
||||
let fs_join = tokio::task::spawn(fs_fut);
|
||||
let aws_join = tokio::task::spawn(aws_fut);
|
||||
read_fut.await;
|
||||
|
||||
let mut new_link = None;
|
||||
|
||||
if let Ok(url) = fs_fut.await {
|
||||
if let Ok(url) = fs_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
if let Ok(url) = aws_fut.await {
|
||||
if let Ok(url) = aws_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
@ -57,31 +46,25 @@ pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: Field,
|
||||
disposition: ContentDisposition,
|
||||
fs: Data<Addr<FileSystemExecutor>>,
|
||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = disposition.get_filename().unwrap();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(4);
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
// Upload to AWS S3
|
||||
let aws_fut = aws_s3(system_file_name, receiver);
|
||||
let aws_fut = aws_s3(system_file_name, amazon, receiver);
|
||||
let read_fut = read_form_data(&mut field, sender);
|
||||
|
||||
read_form_data(&mut field, sender).await;
|
||||
let aws_join = tokio::task::spawn(aws_fut);
|
||||
read_fut.await;
|
||||
|
||||
let new_link = tokio::select! {
|
||||
b = aws_fut => b,
|
||||
};
|
||||
let mut new_link = None;
|
||||
|
||||
if let Ok(url) = aws_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
{
|
||||
use filesystem_actor::RemoveTmpFile;
|
||||
let _ = fs
|
||||
.send(RemoveTmpFile {
|
||||
file_name: format!("{}-{}", user_id, filename),
|
||||
})
|
||||
.await
|
||||
.ok();
|
||||
};
|
||||
Ok(new_link.unwrap_or_default())
|
||||
}
|
||||
|
||||
@ -90,35 +73,25 @@ pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: Field,
|
||||
disposition: ContentDisposition,
|
||||
fs: Data<Addr<FileSystemExecutor>>,
|
||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = disposition.get_filename().unwrap();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(4);
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
let fs_fut = local_storage_write(
|
||||
system_file_name.clone(),
|
||||
fs.clone(),
|
||||
user_id,
|
||||
sender.subscribe(),
|
||||
);
|
||||
let fs_fut = local_storage_write(system_file_name, fs, user_id, sender.subscribe());
|
||||
let read_fut = read_form_data(&mut field, sender);
|
||||
|
||||
read_form_data(&mut field, sender).await;
|
||||
let fs_join = tokio::task::spawn(fs_fut);
|
||||
read_fut.await;
|
||||
|
||||
let new_link = tokio::select! {
|
||||
a = fs_fut => a,
|
||||
};
|
||||
let mut new_link = None;
|
||||
|
||||
if let Ok(url) = fs_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
{
|
||||
use filesystem_actor::RemoveTmpFile;
|
||||
let _ = fs
|
||||
.send(RemoveTmpFile {
|
||||
file_name: format!("{}-{}", user_id, filename),
|
||||
})
|
||||
.await
|
||||
.ok();
|
||||
};
|
||||
Ok(new_link.unwrap_or_default())
|
||||
}
|
||||
|
||||
@ -134,44 +107,28 @@ async fn read_form_data(field: &mut Field, sender: Sender<bytes::Bytes>) {
|
||||
|
||||
/// Stream bytes directly to AWS S3 Service
|
||||
#[cfg(feature = "aws-s3")]
|
||||
async fn aws_s3(system_file_name: String, mut receiver: Receiver<bytes::Bytes>) -> Option<String> {
|
||||
let web_config = jirs_config::web::Configuration::read();
|
||||
let s3 = &web_config.s3;
|
||||
async fn aws_s3(
|
||||
system_file_name: String,
|
||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||
receiver: Receiver<bytes::Bytes>,
|
||||
) -> Option<String> {
|
||||
let s3 = jirs_config::amazon::config();
|
||||
if !s3.active {
|
||||
return None;
|
||||
}
|
||||
s3.set_variables();
|
||||
log::debug!("{:?}", s3);
|
||||
|
||||
let mut v: Vec<u8> = vec![];
|
||||
use bytes::Buf;
|
||||
|
||||
while let Ok(b) = receiver.recv().await {
|
||||
v.extend_from_slice(b.bytes())
|
||||
match amazon
|
||||
.send(amazon_actor::S3PutObject {
|
||||
source: receiver,
|
||||
file_name: system_file_name.to_string(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
// let stream = receiver.into_stream();
|
||||
// let stream = stream.map_err(|_e| std::io::Error::from_raw_os_error(1));
|
||||
|
||||
let client = S3Client::new(s3.region());
|
||||
let put_object = PutObjectRequest {
|
||||
bucket: s3.bucket.clone(),
|
||||
key: system_file_name.clone(),
|
||||
// body: Some(ByteStream::new(stream)),
|
||||
body: Some(v.into()),
|
||||
..Default::default()
|
||||
};
|
||||
let id = match client.put_object(put_object).await {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
log::debug!("{:?}", id);
|
||||
Some(aws_s3_url(system_file_name.as_str(), s3))
|
||||
}
|
||||
|
||||
///
|
||||
#[cfg(feature = "local-storage")]
|
||||
async fn local_storage_write(
|
||||
system_file_name: String,
|
||||
@ -179,36 +136,29 @@ async fn local_storage_write(
|
||||
user_id: jirs_data::UserId,
|
||||
receiver: Receiver<bytes::Bytes>,
|
||||
) -> Option<String> {
|
||||
let web_config = jirs_config::web::Configuration::read();
|
||||
let fs_config = jirs_config::fs::Configuration::read();
|
||||
let web_config = jirs_config::web::config();
|
||||
let fs_config = jirs_config::fs::config();
|
||||
|
||||
let _ = fs
|
||||
match fs
|
||||
.send(filesystem_actor::CreateFile {
|
||||
source: receiver,
|
||||
file_name: system_file_name.clone(),
|
||||
file_name: system_file_name.to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
Some(format!(
|
||||
"{proto}://{bind}{port}{client_path}/{user_id}-{filename}",
|
||||
proto = if web_config.ssl { "https" } else { "http" },
|
||||
bind = web_config.bind,
|
||||
port = match web_config.port.as_str() {
|
||||
"80" | "443" => "".to_string(),
|
||||
p => format!(":{}", p),
|
||||
},
|
||||
client_path = fs_config.client_path,
|
||||
user_id = user_id,
|
||||
filename = system_file_name
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "aws-s3")]
|
||||
fn aws_s3_url(key: &str, config: &AmazonS3Storage) -> String {
|
||||
format!(
|
||||
"https://{bucket}.s3.{region}.amazonaws.com/{key}",
|
||||
bucket = config.bucket,
|
||||
region = config.region_name,
|
||||
key = key
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => Some(format!(
|
||||
"{proto}://{bind}{port}{client_path}/{user_id}-{filename}",
|
||||
proto = if web_config.ssl { "https" } else { "http" },
|
||||
bind = web_config.bind,
|
||||
port = match web_config.port.as_str() {
|
||||
"80" | "443" => "".to_string(),
|
||||
p => format!(":{}", p),
|
||||
},
|
||||
client_path = fs_config.client_path,
|
||||
user_id = user_id,
|
||||
filename = system_file_name
|
||||
)),
|
||||
Ok(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,12 @@ env_logger = "0.7"
|
||||
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||
|
||||
[dependencies.comrak]
|
||||
version = "*"
|
||||
|
||||
[dependencies.pulldown-cmark]
|
||||
version = "*"
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["websocket"]
|
||||
@ -48,3 +54,6 @@ path = "../database-actor"
|
||||
|
||||
[dependencies.mail-actor]
|
||||
path = "../mail-actor"
|
||||
|
||||
[dependencies.highlight-actor]
|
||||
path = "../highlight-actor"
|
||||
|
28
actors/websocket-actor/src/handlers/hi.rs
Normal file
28
actors/websocket-actor/src/handlers/hi.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use futures::executor::block_on;
|
||||
|
||||
use jirs_data::WsMsg;
|
||||
use jirs_data::{Code, Lang};
|
||||
|
||||
use crate::{WebSocketActor, WsHandler, WsResult};
|
||||
|
||||
pub struct HighlightCode(pub Lang, pub Code);
|
||||
|
||||
impl WsHandler<HighlightCode> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> WsResult {
|
||||
self.require_user()?.id;
|
||||
match block_on(self.hi.send(highlight_actor::HighlightCode {
|
||||
code: msg.1,
|
||||
lang: msg.0,
|
||||
})) {
|
||||
Ok(Ok(res)) => Ok(Some(WsMsg::HighlightedCode(res))),
|
||||
Ok(Err(e)) => {
|
||||
error!("{:?}", e);
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -90,7 +90,7 @@ impl WsHandler<CreateInvitation> for WebSocketActor {
|
||||
})) {
|
||||
self.addr.do_send(InnerMsg::SendToUser(
|
||||
message.receiver_id,
|
||||
WsMsg::Message(message),
|
||||
WsMsg::MessageUpdated(message),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,64 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
|
||||
msg.title = Some(s);
|
||||
}
|
||||
(IssueFieldId::Description, PayloadVariant::String(s)) => {
|
||||
msg.description = Some(s);
|
||||
// let mut opts = comrak::ComrakOptions::default();
|
||||
// opts.render.github_pre_lang = true;
|
||||
// let html = comrak::markdown_to_html(s.as_str(), &opts);
|
||||
|
||||
let html: String = {
|
||||
use pulldown_cmark::*;
|
||||
let parser = pulldown_cmark::Parser::new(s.as_str());
|
||||
enum ParseState {
|
||||
Code(highlight_actor::TextHighlightCode),
|
||||
Other,
|
||||
};
|
||||
let mut state = ParseState::Other;
|
||||
|
||||
let parser = parser.flat_map(|event| match event {
|
||||
Event::Text(s) => {
|
||||
if let ParseState::Code(h) = &mut state {
|
||||
h.code.push_str(s.as_ref());
|
||||
return vec![];
|
||||
}
|
||||
vec![Event::Text(s)]
|
||||
}
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(name))) => {
|
||||
state = ParseState::Code(highlight_actor::TextHighlightCode {
|
||||
lang: name.to_string(),
|
||||
code: String::new(),
|
||||
});
|
||||
vec![Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(name)))]
|
||||
}
|
||||
Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => {
|
||||
let ev = if let ParseState::Code(h) = &mut state {
|
||||
let mut msg = highlight_actor::TextHighlightCode {
|
||||
code: String::new(),
|
||||
lang: String::new(),
|
||||
};
|
||||
std::mem::swap(h, &mut msg);
|
||||
let highlighted =
|
||||
match futures::executor::block_on(self.hi.send(msg)) {
|
||||
Ok(Ok(res)) => res,
|
||||
_ => s.to_string(),
|
||||
};
|
||||
vec![
|
||||
Event::Html(highlighted.into()),
|
||||
Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(lang))),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
state = ParseState::Other;
|
||||
ev
|
||||
}
|
||||
_ => vec![event],
|
||||
});
|
||||
let mut buff = String::new();
|
||||
let _ = html::push_html(&mut buff, parser);
|
||||
buff
|
||||
};
|
||||
msg.description = Some(html);
|
||||
msg.description_text = Some(s);
|
||||
}
|
||||
(IssueFieldId::IssueStatusId, PayloadVariant::I32(s)) => {
|
||||
msg.issue_status_id = Some(s);
|
||||
|
@ -1,11 +1,12 @@
|
||||
pub use {
|
||||
auth::*, comments::*, epics::*, invitations::*, issue_statuses::*, issues::*, messages::*,
|
||||
projects::*, user_projects::*, users::*,
|
||||
auth::*, comments::*, epics::*, hi::*, invitations::*, issue_statuses::*, issues::*,
|
||||
messages::*, projects::*, user_projects::*, users::*,
|
||||
};
|
||||
|
||||
pub mod auth;
|
||||
pub mod comments;
|
||||
pub mod epics;
|
||||
pub mod hi;
|
||||
pub mod invitations;
|
||||
pub mod issue_statuses;
|
||||
pub mod issues;
|
||||
|
@ -34,6 +34,7 @@ struct WebSocketActor {
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
mail: Data<Addr<MailExecutor>>,
|
||||
addr: Addr<WsServer>,
|
||||
hi: Data<Addr<highlight_actor::HighlightActor>>,
|
||||
current_user: Option<jirs_data::User>,
|
||||
current_user_project: Option<jirs_data::UserProject>,
|
||||
current_project: Option<jirs_data::Project>,
|
||||
@ -186,6 +187,9 @@ impl WebSocketActor {
|
||||
self.handle_msg(epics::UpdateEpic { epic_id, name }, ctx)?
|
||||
}
|
||||
WsMsg::EpicDelete(epic_id) => self.handle_msg(epics::DeleteEpic { epic_id }, ctx)?,
|
||||
WsMsg::HighlightCode(lang, code) => {
|
||||
self.handle_msg(hi::HighlightCode(lang, code), ctx)?
|
||||
}
|
||||
|
||||
// else fail
|
||||
_ => {
|
||||
@ -323,11 +327,13 @@ pub async fn index(
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
mail: Data<Addr<MailExecutor>>,
|
||||
ws_server: Data<Addr<WsServer>>,
|
||||
hi: Data<Addr<highlight_actor::HighlightActor>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
ws::start(
|
||||
WebSocketActor {
|
||||
db,
|
||||
mail,
|
||||
hi,
|
||||
current_user: None,
|
||||
current_user_project: None,
|
||||
current_project: None,
|
||||
|
@ -2,7 +2,7 @@
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "database-actor/src/schema.rs"
|
||||
file = "actors/database-actor/src/schema.rs"
|
||||
import_types = ["diesel::sql_types::*", "jirs_data::sql::*"]
|
||||
with_docs = true
|
||||
patch_file = "./database-actor/src/schema.patch"
|
||||
patch_file = "./actors/database-actor/src/schema.patch"
|
||||
|
@ -13,12 +13,14 @@ crate-type = ["cdylib", "rlib"]
|
||||
name = "jirs_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
print-model = []
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
jirs-data = { path = "../shared/jirs-data", features = ["frontend"] }
|
||||
|
||||
wee_alloc = "*"
|
||||
|
||||
seed = { version = "0.7.0" }
|
||||
seed = { version = "0.8.0" }
|
||||
|
||||
serde = { version = "*" }
|
||||
serde_json = { version = "*" }
|
||||
@ -27,7 +29,10 @@ bincode = { version = "*" }
|
||||
chrono = { version = "0.4", default-features = false, features = ["serde", "wasmbind"] }
|
||||
uuid = { version = "0.8.1", features = ["serde"] }
|
||||
futures = "^0.1.26"
|
||||
comrak = "*"
|
||||
|
||||
[dependencies.wee_alloc]
|
||||
version = "*"
|
||||
features = ["static_array_backend"]
|
||||
|
||||
[dependencies.wasm-bindgen]
|
||||
version = "*"
|
||||
|
@ -84,9 +84,13 @@
|
||||
color: var(--textMedium);
|
||||
}
|
||||
|
||||
#projectPage > .rows > .row > .epicName {
|
||||
margin: 18px 0 10px 0;
|
||||
}
|
||||
|
||||
#projectPage > .rows > .row > .projectBoardLists {
|
||||
display: flex;
|
||||
margin: 26px -5px 0;
|
||||
margin: 10px -5px 0;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -276,8 +276,8 @@ impl ElementBuilder {
|
||||
pub fn mount(&self) {
|
||||
let source = self.to_js();
|
||||
{
|
||||
use seed::*;
|
||||
log!(source);
|
||||
// use seed::*;
|
||||
// log!(source);
|
||||
}
|
||||
use seed::*;
|
||||
match js_sys::eval(source.as_str()) {
|
||||
|
1
jirs-client/src/images/mod.rs
Normal file
1
jirs-client/src/images/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod project_avatar;
|
100
jirs-client/src/images/project_avatar.rs
Normal file
100
jirs-client/src/images/project_avatar.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use {
|
||||
crate::Msg,
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn render() -> Node<Msg> {
|
||||
seed::svg![
|
||||
attrs![
|
||||
At::ViewBox => "0 0 128 128",
|
||||
At::Version => "1.1",
|
||||
At::Xmlns => "http://www.w3.org/2000/svg",
|
||||
At::Width => "40",
|
||||
At::Height => "40"
|
||||
],
|
||||
defs![rect![attrs![
|
||||
At::Id=>"path-1",
|
||||
At::X=>"0",
|
||||
At::Y=>"0",
|
||||
At::Width=>"128",
|
||||
At::Height=>"128",
|
||||
At::Fill=>"#FF5630"
|
||||
]]],
|
||||
g![
|
||||
attrs![At::Id=>"Page-1", At::Stroke=>"none", At::StrokeWidth=>"1" ,At::Fill=>"none", At::FillRule=>"evenodd"],
|
||||
g![
|
||||
rect![
|
||||
attrs![At::Id=>"path-1", At::X=>"0", At::Y=>"0", At::Width=>"128", At::Height=>"128", At::Fill=>"#FF5630"]
|
||||
],
|
||||
g![
|
||||
attrs![
|
||||
At::Id=>"Settings",
|
||||
At::FillRule=>"nonzero",
|
||||
At::Transform=>"translate(20.000000, 17.000000)"
|
||||
],
|
||||
path![attrs![
|
||||
At::D=>"M74.578,84.289 L72.42,84.289 C70.625,84.289 69.157,82.821 69.157,81.026 L69.157,16.537 C69.157,14.742 70.625,13.274 72.42,13.274 L74.578,13.274 C76.373,13.274 77.841,14.742 77.841,16.537 L77.841,81.026 C77.842,82.82 76.373,84.289 74.578,84.289 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#2A5083"]],
|
||||
path![attrs![
|
||||
At::D=>"M14.252,84.289 L12.094,84.289 C10.299,84.289 8.831,82.821 8.831,81.026 L8.831,16.537 C8.831,14.742 10.299,13.274 12.094,13.274 L14.252,13.274 C16.047,13.274 17.515,14.742 17.515,16.537 L17.515,81.026 C17.515,82.82 16.047,84.289 14.252,84.289 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#2A5083"]],
|
||||
rect![attrs![
|
||||
At::Id=>"Rectangle-path",
|
||||
At::Fill=>"#153A56",
|
||||
At::X=>"8.83",
|
||||
At::Y=>"51.311",
|
||||
At::Width=>"8.685",
|
||||
At::Height=>"7.763"]],
|
||||
path![attrs![
|
||||
At::D=>"M13.173,53.776 L13.173,53.776 C6.342,53.776 0.753,48.187 0.753,41.356 L0.753,41.356 C0.753,34.525 6.342,28.936 13.173,28.936 L13.173,28.936 C20.004,28.936 25.593,34.525 25.593,41.356 L25.593,41.356 C25.593,48.187 20.004,53.776 13.173,53.776 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#FFFFFF"]],
|
||||
path![attrs![
|
||||
At::D=>"M18.021,43.881 L8.324,43.881 C7.453,43.881 6.741,43.169 6.741,42.298 L6.741,41.25 C6.741,40.379 7.453,39.667 8.324,39.667 L18.021,39.667 C18.892,39.667 19.604,40.379 19.604,41.25 L19.604,42.297 C19.605,43.168 18.892,43.881 18.021,43.881 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#2A5083",
|
||||
At::Opacity=>"0.2"]],
|
||||
rect![attrs![
|
||||
At::Id=>"Rectangle-path",
|
||||
At::Fill=>"#153A56",
|
||||
At::X=>"69.157",
|
||||
At::Y=>"68.307",
|
||||
At::Width=>"8.685",
|
||||
At::Height=>"7.763"]],
|
||||
path![attrs![
|
||||
At::D=>"M73.499,70.773 L73.499,70.773 C66.668,70.773 61.079,65.184 61.079,58.353 L61.079,58.353 C61.079,51.522 66.668,45.933 73.499,45.933 L73.499,45.933 C80.33,45.933 85.919,51.522 85.919,58.353 L85.919,58.353 C85.919,65.183 80.33,70.773 73.499,70.773 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#FFFFFF"]],
|
||||
path![attrs![
|
||||
At::D=>"M78.348,60.877 L68.651,60.877 C67.78,60.877 67.068,60.165 67.068,59.294 L67.068,58.247 C67.068,57.376 67.781,56.664 68.651,56.664 L78.348,56.664 C79.219,56.664 79.931,57.377 79.931,58.247 L79.931,59.294 C79.931,60.165 79.219,60.877 78.348,60.877 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#2A5083",
|
||||
At::Opacity=>"0.2"]],
|
||||
path![attrs![
|
||||
At::D=>"M44.415,84.289 L42.257,84.289 C40.462,84.289 38.994,82.821 38.994,81.026 L38.994,16.537 C38.994,14.742 40.462,13.274 42.257,13.274 L44.415,13.274 C46.21,13.274 47.678,14.742 47.678,16.537 L47.678,81.026 C47.678,82.82 46.21,84.289 44.415,84.289 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#2A5083"]],
|
||||
rect![attrs![
|
||||
At::Id=>"Rectangle-path",
|
||||
At::Fill=>"#153A56",
|
||||
At::X=>"38.974",
|
||||
At::Y=>"23.055",
|
||||
At::Width=>"8.685",
|
||||
At::Height=>"7.763"]],
|
||||
path![attrs![
|
||||
At::D=>"M43.316,25.521 L43.316,25.521 C36.485,25.521 30.896,19.932 30.896,13.101 L30.896,13.101 C30.896,6.27 36.485,0.681 43.316,0.681 L43.316,0.681 C50.147,0.681 55.736,6.27 55.736,13.101 L55.736,13.101 C55.736,19.932 50.147,25.521 43.316,25.521 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#FFFFFF"]],
|
||||
path![attrs![
|
||||
At::D=>"M48.165,15.626 L38.468,15.626 C37.597,15.626 36.885,14.914 36.885,14.043 L36.885,12.996 C36.885,12.125 37.597,11.413 38.468,11.413 L48.165,11.413 C49.036,11.413 49.748,12.125 49.748,12.996 L49.748,14.043 C49.748,14.913 49.036,15.626 48.165,15.626 Z",
|
||||
At::Id=>"Shape",
|
||||
At::Fill=>"#2A5083",
|
||||
At::Opacity=>"0.2"]],
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{InviteFieldId, WsMsg};
|
||||
|
||||
use crate::model::{InvitePage, Model, Page, PageContent};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::{outer_layout, write_auth_token, ToNode};
|
||||
use crate::validations::is_token;
|
||||
use crate::ws::send_ws_msg;
|
||||
use crate::{
|
||||
authorize_or_redirect, FieldId, InvitationPageChange, Msg, PageChanged, WebSocketChanged,
|
||||
};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match model.page_content {
|
||||
PageContent::Invite(..) => (),
|
||||
_ if model.page == Page::Invite => build_page_content(model),
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::Invite(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(ws_msg)) => match ws_msg {
|
||||
WsMsg::InvitationAcceptFailure(_) => {
|
||||
page.error = Some("Invalid token".to_string());
|
||||
}
|
||||
WsMsg::InvitationAcceptSuccess(token) => {
|
||||
if let Ok(Msg::AuthTokenStored) = write_auth_token(Some(token)) {
|
||||
authorize_or_redirect(model, orders);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Msg::StrInputChanged(FieldId::Invite(InviteFieldId::Token), text) => {
|
||||
page.token_touched = true;
|
||||
page.token = text;
|
||||
page.error = None;
|
||||
}
|
||||
Msg::PageChanged(PageChanged::Invitation(InvitationPageChange::SubmitForm)) => {
|
||||
if let Ok(token) = uuid::Uuid::from_str(page.token.as_str()) {
|
||||
send_ws_msg(
|
||||
WsMsg::InvitationAcceptRequest(token),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
page.error = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_page_content(model: &mut Model) {
|
||||
let s: String = seed::document().location().unwrap().to_string().into();
|
||||
let url = seed::Url::from_str(s.as_str()).unwrap();
|
||||
let search = url.search();
|
||||
let values = search.get("token").cloned().unwrap_or_default();
|
||||
let mut content = InvitePage::default();
|
||||
content.token = values.get(0).cloned().unwrap_or_default();
|
||||
model.page_content = PageContent::Invite(Box::new(content));
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
PageContent::Invite(page) => page,
|
||||
_ => return empty![],
|
||||
};
|
||||
|
||||
let token_field = token_field(page);
|
||||
let submit_field = submit(page);
|
||||
let error = match page.error.as_ref() {
|
||||
Some(s) => div![class!["error"], s.as_str()],
|
||||
_ => empty![],
|
||||
};
|
||||
|
||||
let form = StyledForm::build()
|
||||
.heading("Welcome in JIRS")
|
||||
.on_submit(ev(Ev::Submit, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::PageChanged(PageChanged::Invitation(InvitationPageChange::SubmitForm))
|
||||
}))
|
||||
.add_field(token_field)
|
||||
.add_field(submit_field)
|
||||
.add_field(error)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
outer_layout(model, "invite", vec![form])
|
||||
}
|
||||
|
||||
fn submit(_page: &InvitePage) -> Node<Msg> {
|
||||
let submit = StyledButton::build()
|
||||
.text("Accept")
|
||||
.primary()
|
||||
.build()
|
||||
.into_node();
|
||||
StyledField::build().input(submit).build().into_node()
|
||||
}
|
||||
|
||||
fn token_field(page: &InvitePage) -> Node<Msg> {
|
||||
let token = StyledInput::build()
|
||||
.valid(!page.token_touched || is_token(page.token.as_str()) && page.error.is_none())
|
||||
.value(page.token.as_str())
|
||||
.build(FieldId::Invite(InviteFieldId::Token))
|
||||
.into_node();
|
||||
|
||||
StyledField::build()
|
||||
.input(token)
|
||||
.label("Your invite token")
|
||||
.build()
|
||||
.into_node()
|
||||
}
|
@ -1,39 +1,61 @@
|
||||
#![feature(or_patterns, type_ascription)]
|
||||
|
||||
use seed::{prelude::*, *};
|
||||
use web_sys::File;
|
||||
use {
|
||||
crate::{
|
||||
model::{ModalType, Model, Page},
|
||||
shared::{
|
||||
go_to_board, go_to_login,
|
||||
styled_date_time_input::StyledDateTimeChanged,
|
||||
styled_select::StyledSelectChanged,
|
||||
styled_tooltip,
|
||||
styled_tooltip::{Variant as StyledTooltip, Variant},
|
||||
},
|
||||
ws::{flush_queue, open_socket, read_incoming, send_ws_msg},
|
||||
},
|
||||
jirs_data::*,
|
||||
seed::{prelude::*, *},
|
||||
web_sys::File,
|
||||
};
|
||||
pub use {changes::*, fields::*, images::*};
|
||||
|
||||
pub use changes::*;
|
||||
pub use fields::*;
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::model::{ModalType, Model, Page};
|
||||
use crate::shared::styled_date_time_input::StyledDateTimeChanged;
|
||||
use crate::shared::{go_to_board, go_to_login, styled_tooltip};
|
||||
// use crate::shared::styled_rte::RteMsg;
|
||||
use crate::shared::styled_select::StyledSelectChanged;
|
||||
use crate::shared::styled_tooltip::{Variant as StyledTooltip, Variant};
|
||||
use crate::ws::{flush_queue, open_socket, read_incoming, send_ws_msg};
|
||||
|
||||
mod changes;
|
||||
pub mod elements;
|
||||
mod fields;
|
||||
mod invite;
|
||||
mod images;
|
||||
mod modal;
|
||||
mod modals;
|
||||
mod model;
|
||||
mod profile;
|
||||
mod project;
|
||||
mod project_settings;
|
||||
mod reports;
|
||||
mod pages;
|
||||
mod shared;
|
||||
mod sign_in;
|
||||
mod sign_up;
|
||||
mod users;
|
||||
pub mod validations;
|
||||
mod ws;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
// #[global_allocator]
|
||||
// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ResourceKind {
|
||||
Issue,
|
||||
IssueStatus,
|
||||
Epic,
|
||||
Project,
|
||||
User,
|
||||
UserProject,
|
||||
Message,
|
||||
Comment,
|
||||
Auth,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OperationKind {
|
||||
ListLoaded,
|
||||
SingleLoaded,
|
||||
SingleCreated,
|
||||
SingleRemoved,
|
||||
SingleModified,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
@ -112,6 +134,9 @@ pub enum Msg {
|
||||
|
||||
// WebSocket
|
||||
WebSocketChange(WebSocketChanged),
|
||||
|
||||
// resource changes
|
||||
ResourceChanged(ResourceKind, OperationKind, Option<i32>),
|
||||
}
|
||||
|
||||
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
@ -139,8 +164,10 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
orders.skip();
|
||||
return;
|
||||
}
|
||||
WebSocketChanged::WsMsg(ref ws_msg) => {
|
||||
WebSocketChanged::WsMsg(ws_msg) => {
|
||||
ws::update(ws_msg, model, orders);
|
||||
orders.skip();
|
||||
return;
|
||||
}
|
||||
WebSocketChanged::WebSocketMessageLoaded(v) => {
|
||||
match bincode::deserialize(v.as_slice()) {
|
||||
@ -187,7 +214,6 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
return;
|
||||
}
|
||||
Msg::ChangePage(page) => {
|
||||
orders.skip();
|
||||
model.page = *page;
|
||||
}
|
||||
Msg::ToggleTooltip(variant) => match variant {
|
||||
@ -203,42 +229,42 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
crate::shared::aside::update(&msg, model, orders);
|
||||
crate::shared::navbar_left::update(&msg, model, orders);
|
||||
crate::modal::update(&msg, model, orders);
|
||||
match model.page {
|
||||
Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders),
|
||||
Page::ProjectSettings => project_settings::update(msg, model, orders),
|
||||
Page::SignIn => sign_in::update(msg, model, orders),
|
||||
Page::SignUp => sign_up::update(msg, model, orders),
|
||||
Page::Invite => invite::update(msg, model, orders),
|
||||
Page::Users => users::update(msg, model, orders),
|
||||
Page::Profile => profile::update(msg, model, orders),
|
||||
Page::Reports => reports::update(msg, model, orders),
|
||||
|
||||
{
|
||||
use crate::shared::{aside, navbar_left};
|
||||
aside::update(&msg, model, orders);
|
||||
navbar_left::update(&msg, model, orders);
|
||||
}
|
||||
if cfg!(debug_assertions) {
|
||||
// debug!(model);
|
||||
crate::modal::update(&msg, model, orders);
|
||||
|
||||
match model.page {
|
||||
Page::Project | Page::AddIssue | Page::EditIssue(..) => {
|
||||
pages::project_page::update(msg, model, orders)
|
||||
}
|
||||
Page::ProjectSettings => pages::project_settings_page::update(msg, model, orders),
|
||||
Page::SignIn => pages::sign_in_page::update(msg, model, orders),
|
||||
Page::SignUp => pages::sign_up_page::update(msg, model, orders),
|
||||
Page::Invite => pages::invite_page::update(msg, model, orders),
|
||||
Page::Users => pages::users_page::update(msg, model, orders),
|
||||
Page::Profile => pages::profile_page::update(msg, model, orders),
|
||||
Page::Reports => pages::reports_page::update(msg, model, orders),
|
||||
}
|
||||
if cfg!(features = "print-model") {
|
||||
log!(model);
|
||||
}
|
||||
}
|
||||
|
||||
fn view(model: &model::Model) -> Node<Msg> {
|
||||
match model.page {
|
||||
Page::Project | Page::AddIssue => project::view(model),
|
||||
Page::EditIssue(_id) => project::view(model),
|
||||
Page::ProjectSettings => project_settings::view(model),
|
||||
Page::SignIn => sign_in::view(model),
|
||||
Page::SignUp => sign_up::view(model),
|
||||
Page::Invite => invite::view(model),
|
||||
Page::Users => users::view(model),
|
||||
Page::Profile => profile::view(model),
|
||||
Page::Reports => reports::view(model),
|
||||
}
|
||||
}
|
||||
|
||||
fn routes(url: Url) -> Option<Msg> {
|
||||
match resolve_page(url) {
|
||||
Some(page) => Some(Msg::ChangePage(page)),
|
||||
_ => None,
|
||||
Page::Project | Page::AddIssue => pages::project_page::view(model),
|
||||
Page::EditIssue(_id) => pages::project_page::view(model),
|
||||
Page::ProjectSettings => pages::project_settings_page::view(model),
|
||||
Page::SignIn => pages::sign_in_page::view(model),
|
||||
Page::SignUp => pages::sign_up_page::view(model),
|
||||
Page::Invite => pages::invite_page::view(model),
|
||||
Page::Users => pages::users_page::view(model),
|
||||
Page::Profile => pages::profile_page::view(model),
|
||||
Page::Reports => pages::reports_page::view(model),
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,14 +303,39 @@ pub fn render(host_url: String, ws_url: String) {
|
||||
}
|
||||
elements::define();
|
||||
|
||||
let _app = seed::App::builder(update, view)
|
||||
.routes(routes)
|
||||
.after_mount(after_mount)
|
||||
.window_events(window_events)
|
||||
.build_and_start();
|
||||
let app = seed::App::start("app", init, update, view);
|
||||
|
||||
{
|
||||
let app_clone = app.clone();
|
||||
let on_key_down = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
|
||||
let event: web_sys::KeyboardEvent = event.unchecked_into();
|
||||
|
||||
let tag_name: String = seed::document()
|
||||
.active_element()
|
||||
.map(|el| el.tag_name())
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = match tag_name.to_lowercase().as_str() {
|
||||
"input" | "textarea" => return,
|
||||
_ => event.key(),
|
||||
};
|
||||
|
||||
let msg = Msg::GlobalKeyDown {
|
||||
key,
|
||||
shift: event.shift_key(),
|
||||
ctrl: event.ctrl_key(),
|
||||
alt: event.alt_key(),
|
||||
};
|
||||
app_clone.update(msg);
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
seed::body()
|
||||
.add_event_listener_with_callback("keyup", on_key_down.as_ref().unchecked_ref())
|
||||
.expect("Failed to mount global key handler");
|
||||
on_key_down.forget();
|
||||
}
|
||||
}
|
||||
|
||||
fn after_mount(url: Url, orders: &mut impl Orders<Msg>) -> AfterMount<Model> {
|
||||
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
let host_url = unsafe { HOST_URL.clone() };
|
||||
let ws_url = unsafe { WS_URL.clone() };
|
||||
let mut model = Model::new(host_url, ws_url);
|
||||
@ -294,31 +345,13 @@ fn after_mount(url: Url, orders: &mut impl Orders<Msg>) -> AfterMount<Model> {
|
||||
}
|
||||
model.page = resolve_page(url).unwrap_or(Page::Project);
|
||||
open_socket(&mut model, orders);
|
||||
AfterMount::new(model).url_handling(UrlHandling::PassToRoutes)
|
||||
}
|
||||
|
||||
fn window_events(_model: &Model) -> Vec<EventHandler<Msg>> {
|
||||
vec![keyboard_ev(
|
||||
Ev::KeyDown,
|
||||
move |event: web_sys::KeyboardEvent| {
|
||||
let tag_name: String = seed::document()
|
||||
.active_element()
|
||||
.map(|el| el.tag_name())
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = match tag_name.to_lowercase().as_str() {
|
||||
"" | "input" | "textarea" => return None,
|
||||
_ => event.key(),
|
||||
};
|
||||
|
||||
Some(Msg::GlobalKeyDown {
|
||||
key,
|
||||
shift: event.shift_key(),
|
||||
ctrl: event.ctrl_key(),
|
||||
alt: event.alt_key(),
|
||||
})
|
||||
},
|
||||
)]
|
||||
// orders.subscribe(|subs::UrlChanged(url)| {
|
||||
// if let Some(page) = resolve_page(url) {
|
||||
// orders.send_msg(Msg::ChangePage(page));
|
||||
// }
|
||||
// });
|
||||
model
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -1,16 +1,13 @@
|
||||
use seed::prelude::Node;
|
||||
|
||||
use jirs_data::EpicId;
|
||||
|
||||
use crate::{
|
||||
model::{IssueModal, Model},
|
||||
shared::{styled_field::StyledField, styled_select::StyledSelect, ToChild, ToNode},
|
||||
FieldId, Msg,
|
||||
use {
|
||||
crate::{
|
||||
model::{IssueModal, Model},
|
||||
shared::{styled_field::StyledField, styled_select::StyledSelect, ToChild, ToNode},
|
||||
FieldId, Msg,
|
||||
},
|
||||
jirs_data::EpicId,
|
||||
seed::prelude::Node,
|
||||
};
|
||||
|
||||
pub mod add_issue;
|
||||
pub mod issue_details;
|
||||
|
||||
pub fn epic_field<Modal>(model: &Model, modal: &Modal, field_id: FieldId) -> Option<Node<Msg>>
|
||||
where
|
||||
Modal: IssueModal,
|
||||
|
@ -1,822 +0,0 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::{
|
||||
modal::{issues::epic_field, time_tracking::time_tracking_field},
|
||||
model::{CommentForm, EditIssueModal, IssueModal, ModalType, Model},
|
||||
shared::{
|
||||
styled_avatar::StyledAvatar,
|
||||
styled_button::StyledButton,
|
||||
styled_editor::StyledEditor,
|
||||
styled_field::StyledField,
|
||||
styled_icon::Icon,
|
||||
styled_input::StyledInput,
|
||||
styled_select::{StyledSelect, StyledSelectChanged},
|
||||
styled_textarea::StyledTextarea,
|
||||
tracking_widget::tracking_link,
|
||||
ToChild, ToNode,
|
||||
},
|
||||
ws::send_ws_msg,
|
||||
EditIssueModalSection, FieldChange, FieldId, Msg, WebSocketChanged,
|
||||
};
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
let modal: &mut EditIssueModal = match model.modals.get_mut(0) {
|
||||
Some(ModalType::EditIssue(_issue_id, modal)) => modal,
|
||||
_ => return,
|
||||
};
|
||||
modal.update_states(msg, orders);
|
||||
|
||||
match msg {
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
|
||||
modal.payload = issue.clone().into();
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.issue_type = (*value).into();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Type,
|
||||
PayloadVariant::IssueType(modal.payload.issue_type),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.issue_status_id = *value as IssueStatusId;
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::IssueStatusId,
|
||||
PayloadVariant::I32(modal.payload.issue_status_id),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.reporter_id = *value as i32;
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Reporter,
|
||||
PayloadVariant::I32(modal.payload.reporter_id),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.user_ids.push(*value as i32);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Assignees,
|
||||
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||
StyledSelectChanged::RemoveMulti(value),
|
||||
) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut modal.payload.user_ids);
|
||||
let dropped = *value as i32;
|
||||
for id in old.into_iter() {
|
||||
if id != dropped {
|
||||
modal.payload.user_ids.push(id);
|
||||
}
|
||||
}
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Assignees,
|
||||
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.priority = (*value).into();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Priority,
|
||||
PayloadVariant::IssuePriority(modal.payload.priority),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
|
||||
value,
|
||||
) => {
|
||||
modal.payload.title = value.clone();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Title,
|
||||
PayloadVariant::String(modal.payload.title.clone()),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
|
||||
value,
|
||||
) => {
|
||||
modal.payload.description = Some(value.clone());
|
||||
modal.payload.description_text = Some(value.clone());
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Description,
|
||||
PayloadVariant::String(
|
||||
modal
|
||||
.payload
|
||||
.description
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
// TimeSpent
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
|
||||
..,
|
||||
) => {
|
||||
modal.payload.time_spent = modal.time_spent.represent_f64_as_i32();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::TimeSpent,
|
||||
PayloadVariant::OptionI32(modal.payload.time_spent),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
|
||||
StyledSelectChanged::Changed(..),
|
||||
) => {
|
||||
modal.payload.time_spent = modal.time_spent_select.values.get(0).map(|n| *n as i32);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::TimeSpent,
|
||||
PayloadVariant::OptionI32(modal.payload.time_spent),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
// Time Remaining
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
|
||||
..,
|
||||
) => {
|
||||
modal.payload.time_remaining = modal.time_remaining.represent_f64_as_i32();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::TimeRemaining,
|
||||
PayloadVariant::OptionI32(modal.payload.time_remaining),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
|
||||
StyledSelectChanged::Changed(..),
|
||||
) => {
|
||||
modal.payload.time_remaining =
|
||||
modal.time_remaining_select.values.get(0).map(|n| *n as i32);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::TimeRemaining,
|
||||
PayloadVariant::OptionI32(modal.payload.time_remaining),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
// Estimate
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
..,
|
||||
) => {
|
||||
modal.payload.estimate = modal.estimate.represent_f64_as_i32();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Estimate,
|
||||
PayloadVariant::OptionI32(modal.payload.estimate),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
StyledSelectChanged::Changed(..),
|
||||
) => {
|
||||
modal.payload.estimate = modal.estimate_select.values.get(0).map(|n| *n as i32);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Estimate,
|
||||
PayloadVariant::OptionI32(modal.payload.estimate),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||
StyledSelectChanged::Changed(v),
|
||||
) => {
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::EpicName,
|
||||
PayloadVariant::OptionI32(v.map(|n| n as EpicId).clone()),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::ModalChanged(FieldChange::TabChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
|
||||
mode,
|
||||
)) => {
|
||||
modal.description_editor_mode = mode.clone();
|
||||
}
|
||||
Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
flag,
|
||||
)) => {
|
||||
modal.comment_form.creating = *flag;
|
||||
if !*flag {
|
||||
modal.comment_form.body.clear();
|
||||
modal.comment_form.id = None;
|
||||
}
|
||||
}
|
||||
//
|
||||
// comments
|
||||
//
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
text,
|
||||
) => {
|
||||
modal.comment_form.body = text.clone();
|
||||
}
|
||||
Msg::SaveComment => {
|
||||
let msg = match modal.comment_form.id {
|
||||
Some(id) => WsMsg::CommentUpdate(UpdateCommentPayload {
|
||||
id,
|
||||
body: modal.comment_form.body.clone(),
|
||||
}),
|
||||
_ => WsMsg::CommentCreate(CreateCommentPayload {
|
||||
user_id: None,
|
||||
body: modal.comment_form.body.clone(),
|
||||
issue_id: modal.id,
|
||||
}),
|
||||
};
|
||||
send_ws_msg(msg, model.ws.as_ref(), orders);
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
false,
|
||||
)));
|
||||
}
|
||||
Msg::ModalChanged(FieldChange::EditComment(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
comment_id,
|
||||
)) => {
|
||||
let id = *comment_id;
|
||||
let body = model
|
||||
.comments
|
||||
.iter()
|
||||
.find(|c| c.id == id)
|
||||
.map(|c| c.body.clone())
|
||||
.unwrap_or_default();
|
||||
modal.comment_form.body = body;
|
||||
modal.comment_form.id = Some(id);
|
||||
modal.comment_form.creating = true;
|
||||
}
|
||||
Msg::DeleteComment(comment_id) => {
|
||||
send_ws_msg(WsMsg::CommentDelete(*comment_id), model.ws.as_ref(), orders);
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
// global
|
||||
Msg::GlobalKeyDown { key, .. } if key.as_str() == "m" && !modal.comment_form.creating => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
true,
|
||||
)));
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
div![
|
||||
class!["issueDetails"],
|
||||
top_modal_row(model, modal),
|
||||
div![
|
||||
class!["content"],
|
||||
left_modal_column(model, modal),
|
||||
right_modal_column(model, modal),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
id,
|
||||
payload,
|
||||
top_type_state,
|
||||
link_copied,
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let issue_id = *id;
|
||||
|
||||
let click_handler = mouse_ev(Ev::Click, move |_| {
|
||||
let link = format!("http://localhost:7000/issues/{id}", id = issue_id);
|
||||
let el = match seed::html_document().create_element("textarea") {
|
||||
Ok(el) => el
|
||||
.dyn_ref::<web_sys::HtmlTextAreaElement>()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
_ => return None as Option<Msg>,
|
||||
};
|
||||
seed::body().append_child(&el).unwrap();
|
||||
el.set_text_content(Some(link.as_str()));
|
||||
el.select();
|
||||
el.set_selection_range(0, 9999).unwrap();
|
||||
seed::html_document().exec_command("copy").unwrap();
|
||||
seed::body().remove_child(&el).unwrap();
|
||||
Some(Msg::ModalChanged(FieldChange::LinkCopied(
|
||||
FieldId::CopyButtonLabel,
|
||||
true,
|
||||
)))
|
||||
});
|
||||
let close_handler = mouse_ev(Ev::Click, |_| Msg::ModalDropped);
|
||||
let delete_confirmation_handler = mouse_ev(Ev::Click, move |_| {
|
||||
Msg::ModalOpened(Box::new(ModalType::DeleteIssueConfirm(issue_id)))
|
||||
});
|
||||
|
||||
let copy_button = StyledButton::build()
|
||||
.empty()
|
||||
.icon(Icon::Link)
|
||||
.on_click(click_handler)
|
||||
.children(vec![span![if *link_copied {
|
||||
"Link Copied"
|
||||
} else {
|
||||
"Copy link"
|
||||
}]])
|
||||
.build()
|
||||
.into_node();
|
||||
let delete_button = StyledButton::build()
|
||||
.empty()
|
||||
.icon(Icon::Trash.into_styled_builder().size(19).build())
|
||||
.on_click(delete_confirmation_handler)
|
||||
.build()
|
||||
.into_node();
|
||||
let close_button = StyledButton::build()
|
||||
.empty()
|
||||
.icon(Icon::Close.into_styled_builder().size(24).build())
|
||||
.on_click(close_handler)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let issue_types = IssueType::ordered();
|
||||
let issue_type_select = StyledSelect::build()
|
||||
.dropdown_width(150)
|
||||
.name("type")
|
||||
.text_filter(top_type_state.text_filter.as_str())
|
||||
.opened(top_type_state.opened)
|
||||
.valid(true)
|
||||
.options(
|
||||
issue_types
|
||||
.iter()
|
||||
.map(|t| t.to_child().name("type"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![{
|
||||
let id = modal.id;
|
||||
let issue_type = &payload.issue_type;
|
||||
issue_type
|
||||
.to_child()
|
||||
.name("type")
|
||||
.text_owned(format!("{} - {}", issue_type, id))
|
||||
}])
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Type,
|
||||
)))
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "topActions"],
|
||||
issue_type_select,
|
||||
div![
|
||||
attrs![At::Class => "topActionsRight"],
|
||||
copy_button,
|
||||
delete_button,
|
||||
close_button
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
payload,
|
||||
description_editor_mode,
|
||||
comment_form,
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let title = StyledInput::build()
|
||||
.add_input_class("issueSummary")
|
||||
.add_wrapper_class("issueSummary")
|
||||
.add_wrapper_class("textarea")
|
||||
.value(payload.title.as_str())
|
||||
.valid(payload.title.len() >= 3)
|
||||
.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(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()
|
||||
.add_class("userAvatar")
|
||||
.size(32)
|
||||
.avatar_url(
|
||||
model
|
||||
.user
|
||||
.as_ref()
|
||||
.and_then(|u| u.avatar_url.as_deref())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let create_comment = if comment_form.creating && comment_form.id.is_none() {
|
||||
build_comment_form(comment_form)
|
||||
} else {
|
||||
let creating_comment = comment_form.creating;
|
||||
let handler = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
!creating_comment,
|
||||
))
|
||||
});
|
||||
vec![div![class!["fakeTextArea"], "Add a comment...", handler]]
|
||||
};
|
||||
|
||||
let comments: Vec<Node<Msg>> = model
|
||||
.comments
|
||||
.iter()
|
||||
.flat_map(|c| comment(model, modal, c))
|
||||
.collect();
|
||||
|
||||
div![
|
||||
class!["left"],
|
||||
title,
|
||||
description_field,
|
||||
div![
|
||||
class!["comments"],
|
||||
div![class!["title"], "Comments"],
|
||||
div![
|
||||
class!["create"],
|
||||
user_avatar,
|
||||
div![
|
||||
class!["right"],
|
||||
create_comment,
|
||||
div![
|
||||
class!["proTip"],
|
||||
strong![class!["strong"], "Pro tip: "],
|
||||
"press ",
|
||||
span![class!["tipLetter"], "M"],
|
||||
" to comment"
|
||||
]
|
||||
]
|
||||
],
|
||||
comments
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
|
||||
let submit_comment_form = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::SaveComment
|
||||
});
|
||||
let close_comment_form = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
false,
|
||||
))
|
||||
});
|
||||
|
||||
let text_area = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Comment(
|
||||
CommentFieldId::Body,
|
||||
)))
|
||||
.value(form.body.as_str())
|
||||
.placeholder("Add a comment...")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let submit = StyledButton::build()
|
||||
.primary()
|
||||
.on_click(submit_comment_form)
|
||||
.text("Save")
|
||||
.build()
|
||||
.into_node();
|
||||
let cancel = StyledButton::build()
|
||||
.empty()
|
||||
.on_click(close_comment_form)
|
||||
.text("Cancel")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
vec![text_area, div![class!["actions"], submit, cancel]]
|
||||
}
|
||||
|
||||
fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Option<Node<Msg>> {
|
||||
let show_form = modal.comment_form.creating && modal.comment_form.id == Some(comment.id);
|
||||
|
||||
let user = model.users.iter().find(|u| u.id == comment.user_id)?;
|
||||
|
||||
let avatar = StyledAvatar::build()
|
||||
.size(32)
|
||||
.avatar_url(user.avatar_url.as_deref()?)
|
||||
.add_class("userAvatar")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let buttons = if model.user.as_ref().map(|u| u.id) == Some(comment.user_id) {
|
||||
let comment_id = comment.id;
|
||||
let delete_comment_handler = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalOpened(Box::new(ModalType::DeleteCommentConfirm(comment_id)))
|
||||
});
|
||||
let edit_button = StyledButton::build()
|
||||
.add_class("editButton")
|
||||
.on_click(mouse_ev(Ev::Click, move |_| {
|
||||
Msg::ModalChanged(FieldChange::EditComment(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
comment_id,
|
||||
))
|
||||
}))
|
||||
.text("Edit")
|
||||
.empty()
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let cancel_button = StyledButton::build()
|
||||
.add_class("deleteButton")
|
||||
.on_click(delete_comment_handler)
|
||||
.text("Delete")
|
||||
.empty()
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
vec![edit_button, cancel_button]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let content = if show_form {
|
||||
div![class!["content"], build_comment_form(&modal.comment_form)]
|
||||
} else {
|
||||
div![
|
||||
class!["content"],
|
||||
div![class!["userName"], user.name.as_str()],
|
||||
p![class!["body"], comment.body.as_str()],
|
||||
buttons,
|
||||
]
|
||||
};
|
||||
|
||||
let node = div![class!["styledComment"], avatar, content];
|
||||
Some(node)
|
||||
}
|
||||
|
||||
fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
payload,
|
||||
status_state,
|
||||
reporter_state,
|
||||
assignees_state,
|
||||
priority_state,
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let status = StyledSelect::build()
|
||||
.name("status")
|
||||
.opened(status_state.opened)
|
||||
.normal()
|
||||
.text_filter(status_state.text_filter.as_str())
|
||||
.options(
|
||||
model
|
||||
.issue_statuses
|
||||
.iter()
|
||||
.map(|opt| opt.to_child().name("status"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.issue_statuses
|
||||
.iter()
|
||||
.filter(|is| is.id == payload.issue_status_id)
|
||||
.map(|is| is.to_child().name("status"))
|
||||
.collect(),
|
||||
)
|
||||
.valid(true)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::IssueStatusId,
|
||||
)))
|
||||
.into_node();
|
||||
let status_field = StyledField::build()
|
||||
.input(status)
|
||||
.label("Status")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let assignees = StyledSelect::build()
|
||||
.name("assignees")
|
||||
.opened(assignees_state.opened)
|
||||
.empty()
|
||||
.multi()
|
||||
.text_filter(assignees_state.text_filter.as_str())
|
||||
.options(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| user.to_child().name("assignees"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.filter(|user| payload.user_ids.contains(&user.id))
|
||||
.map(|user| user.to_child().name("assignees"))
|
||||
.collect(),
|
||||
)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Assignees,
|
||||
)))
|
||||
.into_node();
|
||||
let assignees_field = StyledField::build()
|
||||
.input(assignees)
|
||||
.label("Assignees")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let reporter = StyledSelect::build()
|
||||
.name("reporter")
|
||||
.opened(reporter_state.opened)
|
||||
.empty()
|
||||
.text_filter(reporter_state.text_filter.as_str())
|
||||
.options(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| user.to_child().name("reporter"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.filter(|user| payload.reporter_id == user.id)
|
||||
.map(|user| user.to_child().name("reporter"))
|
||||
.collect(),
|
||||
)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Reporter,
|
||||
)))
|
||||
.into_node();
|
||||
let reporter_field = StyledField::build()
|
||||
.input(reporter)
|
||||
.label("Reporter")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let issue_priorities = IssuePriority::ordered();
|
||||
let priority = StyledSelect::build()
|
||||
.name("priority")
|
||||
.opened(priority_state.opened)
|
||||
.empty()
|
||||
.text_filter(priority_state.text_filter.as_str())
|
||||
.options(
|
||||
issue_priorities
|
||||
.iter()
|
||||
.map(|p| p.to_child().name("priority"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![payload.priority.to_child().name("priority")])
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Priority,
|
||||
)))
|
||||
.into_node();
|
||||
let priority_field = StyledField::build()
|
||||
.input(priority)
|
||||
.label("Priority")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let time_tracking_type = model
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|p| p.time_tracking)
|
||||
.unwrap_or_else(|| TimeTracking::Untracked);
|
||||
|
||||
let (estimate_field, tracking_field) = if time_tracking_type != TimeTracking::Untracked {
|
||||
let estimate_field = time_tracking_field(
|
||||
time_tracking_type,
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
"Original Estimate (hours)",
|
||||
&modal.estimate,
|
||||
&modal.estimate_select,
|
||||
);
|
||||
|
||||
let tracking = tracking_link(model, modal);
|
||||
let tracking_field = StyledField::build()
|
||||
.label("TIME TRACKING")
|
||||
.tip("")
|
||||
.input(tracking)
|
||||
.build()
|
||||
.into_node();
|
||||
(estimate_field, tracking_field)
|
||||
} else {
|
||||
(empty![], empty![])
|
||||
};
|
||||
|
||||
let epic_field = epic_field(
|
||||
model,
|
||||
modal,
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||
)
|
||||
.unwrap_or_else(|| empty![]);
|
||||
|
||||
div![
|
||||
attrs![At::Class => "right"],
|
||||
status_field,
|
||||
assignees_field,
|
||||
reporter_field,
|
||||
priority_field,
|
||||
estimate_field,
|
||||
tracking_field,
|
||||
epic_field,
|
||||
]
|
||||
}
|
@ -3,8 +3,7 @@ use seed::{prelude::*, *};
|
||||
use jirs_data::{TimeTracking, WsMsg};
|
||||
|
||||
use crate::{
|
||||
modal::issues::*,
|
||||
model::{self, AddIssueModal, EditIssueModal, ModalType, Model, Page},
|
||||
model::{self, ModalType, Model, Page},
|
||||
shared::{
|
||||
find_issue, go_to_board,
|
||||
styled_confirm_modal::StyledConfirmModal,
|
||||
@ -18,7 +17,6 @@ use crate::{
|
||||
mod confirm_delete_issue;
|
||||
#[cfg(debug_assertions)]
|
||||
mod debug_modal;
|
||||
mod delete_issue_status;
|
||||
pub mod issues;
|
||||
pub mod time_tracking;
|
||||
|
||||
@ -57,7 +55,7 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
|
||||
}
|
||||
|
||||
Msg::ChangePage(Page::AddIssue) => {
|
||||
let mut modal = AddIssueModal::default();
|
||||
let mut modal = crate::modals::issues_create::Model::default();
|
||||
modal.project_id = model.project.as_ref().map(|p| p.id);
|
||||
model.modals.push(ModalType::AddIssue(Box::new(modal)));
|
||||
}
|
||||
@ -69,24 +67,31 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
Msg::GlobalKeyDown { key, .. } if key.eq(">") => {
|
||||
orders.skip();
|
||||
log!(model);
|
||||
}
|
||||
Msg::GlobalKeyDown { .. } => {
|
||||
orders.skip();
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
add_issue::update(msg, model, orders);
|
||||
issue_details::update(msg, model, orders);
|
||||
delete_issue_status::update(msg, model, orders);
|
||||
|
||||
use crate::modals::{issue_statuses_delete, issues_create, issues_edit};
|
||||
issues_create::update(msg, model, orders);
|
||||
issues_edit::update(msg, model, orders);
|
||||
issue_statuses_delete::update(msg, model, orders);
|
||||
}
|
||||
|
||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
use crate::modals::{issue_statuses_delete, issues_create, issues_edit};
|
||||
let modals: Vec<Node<Msg>> = model
|
||||
.modals
|
||||
.iter()
|
||||
.map(|modal| match modal {
|
||||
ModalType::EditIssue(issue_id, modal) => {
|
||||
if let Some(_issue) = find_issue(model, *issue_id) {
|
||||
let details = issue_details::view(model, &modal);
|
||||
let details = issues_edit::view(model, modal.as_ref());
|
||||
StyledModal::build()
|
||||
.variant(ModalVariant::Center)
|
||||
.width(1040)
|
||||
@ -98,7 +103,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
}
|
||||
}
|
||||
ModalType::DeleteIssueConfirm(_id) => confirm_delete_issue::view(model),
|
||||
ModalType::AddIssue(modal) => add_issue::view(model, modal),
|
||||
ModalType::AddIssue(modal) => issues_create::view(model, modal),
|
||||
ModalType::DeleteCommentConfirm(comment_id) => {
|
||||
let comment_id = *comment_id;
|
||||
StyledConfirmModal::build()
|
||||
@ -111,7 +116,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
}
|
||||
ModalType::TimeTracking(issue_id) => time_tracking::view(model, *issue_id),
|
||||
ModalType::DeleteIssueStatusModal(delete_issue_modal) => {
|
||||
delete_issue_status::view(model, delete_issue_modal.delete_id)
|
||||
issue_statuses_delete::view(model, delete_issue_modal.delete_id)
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
ModalType::DebugModal => debug_modal::view(model),
|
||||
@ -133,7 +138,10 @@ fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders<Ms
|
||||
};
|
||||
ModalType::EditIssue(
|
||||
issue_id,
|
||||
Box::new(EditIssueModal::new(issue, time_tracking_type)),
|
||||
Box::new(crate::modals::issues_edit::Model::new(
|
||||
issue,
|
||||
time_tracking_type,
|
||||
)),
|
||||
)
|
||||
};
|
||||
send_ws_msg(
|
||||
|
@ -38,7 +38,7 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
|
||||
.map(|p| p.time_tracking)
|
||||
.unwrap_or_else(|| TimeTracking::Untracked);
|
||||
|
||||
let modal_title = div![class!["modalTitle"], "Time tracking"];
|
||||
let modal_title = div![C!["modalTitle"], "Time tracking"];
|
||||
|
||||
let tracking = tracking_widget(model, edit_issue_modal);
|
||||
|
||||
@ -58,9 +58,9 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
|
||||
);
|
||||
|
||||
let inputs = div![
|
||||
class!["inputs"],
|
||||
div![class!["inputContainer"], time_spent_field],
|
||||
div![class!["inputContainer"], time_remaining_field]
|
||||
C!["inputs"],
|
||||
div![C!["inputContainer"], time_spent_field],
|
||||
div![C!["inputContainer"], time_remaining_field]
|
||||
];
|
||||
|
||||
let close = StyledButton::build()
|
||||
@ -75,7 +75,7 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
|
||||
modal_title,
|
||||
tracking,
|
||||
inputs,
|
||||
div![class!["actions"], close],
|
||||
div![C!["actions"], close],
|
||||
])
|
||||
.width(400)
|
||||
.build()
|
||||
|
@ -1,5 +1,7 @@
|
||||
pub use model::*;
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
mod model;
|
||||
mod update;
|
||||
mod view;
|
16
jirs-client/src/modals/issue_statuses_delete/model.rs
Normal file
16
jirs-client/src/modals/issue_statuses_delete/model.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use jirs_data::IssueStatusId;
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub struct Model {
|
||||
pub delete_id: IssueStatusId,
|
||||
pub receiver_id: Option<IssueStatusId>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn new(delete_id: IssueStatusId) -> Self {
|
||||
Self {
|
||||
delete_id,
|
||||
receiver_id: None,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::{IssueStatusId, WsMsg};
|
||||
|
||||
use crate::model::{DeleteIssueStatusModal, ModalType, Model};
|
||||
use crate::shared::styled_confirm_modal::StyledConfirmModal;
|
||||
use crate::shared::ToNode;
|
||||
use crate::{model, Msg, WebSocketChanged};
|
||||
use {
|
||||
crate::{
|
||||
modals::issue_statuses_delete::Model as DeleteIssueStatusModal,
|
||||
model::{ModalType, Model},
|
||||
Msg, WebSocketChanged,
|
||||
},
|
||||
jirs_data::WsMsg,
|
||||
seed::prelude::*,
|
||||
};
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
let _modal: &mut Box<DeleteIssueStatusModal> =
|
||||
@ -34,16 +35,3 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn view(_model: &model::Model, issue_status_id: IssueStatusId) -> Node<Msg> {
|
||||
StyledConfirmModal::build()
|
||||
.title("Delete column")
|
||||
.cancel_text("No")
|
||||
.confirm_text("Yes")
|
||||
.on_confirm(mouse_ev(Ev::Click, move |_| {
|
||||
Msg::DeleteIssueStatus(issue_status_id)
|
||||
}))
|
||||
.message("Are you sure you want to delete column?")
|
||||
.build()
|
||||
.into_node()
|
||||
}
|
22
jirs-client/src/modals/issue_statuses_delete/view.rs
Normal file
22
jirs-client/src/modals/issue_statuses_delete/view.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::IssueStatusId;
|
||||
|
||||
use crate::{
|
||||
model,
|
||||
shared::{styled_confirm_modal::StyledConfirmModal, ToNode},
|
||||
Msg,
|
||||
};
|
||||
|
||||
pub fn view(_model: &model::Model, issue_status_id: IssueStatusId) -> Node<Msg> {
|
||||
StyledConfirmModal::build()
|
||||
.title("Delete column")
|
||||
.cancel_text("No")
|
||||
.confirm_text("Yes")
|
||||
.on_confirm(mouse_ev(Ev::Click, move |_| {
|
||||
Msg::DeleteIssueStatus(issue_status_id)
|
||||
}))
|
||||
.message("Are you sure you want to delete column?")
|
||||
.build()
|
||||
.into_node()
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
pub use model::*;
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
mod model;
|
||||
mod update;
|
||||
mod view;
|
195
jirs-client/src/modals/issues_create/model.rs
Normal file
195
jirs-client/src/modals/issues_create/model.rs
Normal file
@ -0,0 +1,195 @@
|
||||
use {
|
||||
crate::{
|
||||
model::IssueModal,
|
||||
shared::{
|
||||
styled_date_time_input::StyledDateTimeInputState,
|
||||
styled_input::StyledInputState,
|
||||
styled_select::StyledSelectState,
|
||||
styled_select_child::{StyledSelectChild, StyledSelectChildBuilder},
|
||||
ToChild, ToNode,
|
||||
},
|
||||
FieldId, Msg,
|
||||
},
|
||||
jirs_data::{IssueFieldId, IssuePriority},
|
||||
seed::prelude::*,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Type {
|
||||
Task,
|
||||
Bug,
|
||||
Story,
|
||||
Epic,
|
||||
}
|
||||
|
||||
impl From<u32> for Type {
|
||||
fn from(n: u32) -> Self {
|
||||
match n {
|
||||
0 => Type::Task,
|
||||
1 => Type::Bug,
|
||||
2 => Type::Story,
|
||||
3 => Type::Epic,
|
||||
_ => Type::Task,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub(crate) fn ordered<'l>() -> &'l [Type] {
|
||||
use Type::*;
|
||||
&[Task, Bug, Story, Epic]
|
||||
}
|
||||
|
||||
pub(crate) fn submit_label(&self) -> &str {
|
||||
use Type::*;
|
||||
match self {
|
||||
Epic => "Create epic",
|
||||
Bug | Task | Story => "Create issue",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn form_label(&self) -> &str {
|
||||
use Type::*;
|
||||
match self {
|
||||
Epic => "Create epic",
|
||||
Bug | Task | Story => "Create issue",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn submit_action(&self) -> Msg {
|
||||
use Type::*;
|
||||
match self {
|
||||
Epic => Msg::AddEpic,
|
||||
Bug | Task | Story => Msg::AddIssue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> ToChild<'l> for Type {
|
||||
type Builder = StyledSelectChildBuilder<'l>;
|
||||
|
||||
fn to_child<'m: 'l>(&'m self) -> Self::Builder {
|
||||
let name = match self {
|
||||
Type::Task => "Task",
|
||||
Type::Bug => "Bug",
|
||||
Type::Story => "Story",
|
||||
Type::Epic => "Epic",
|
||||
};
|
||||
let value = match self {
|
||||
Type::Task => 0,
|
||||
Type::Bug => 1,
|
||||
Type::Story => 2,
|
||||
Type::Epic => 3,
|
||||
};
|
||||
let icon = match self {
|
||||
Type::Task => crate::shared::styled_icon::Icon::Task,
|
||||
Type::Bug => crate::shared::styled_icon::Icon::Bug,
|
||||
Type::Story => crate::shared::styled_icon::Icon::Story,
|
||||
Type::Epic => crate::shared::styled_icon::Icon::Epic,
|
||||
};
|
||||
|
||||
let type_icon = crate::shared::styled_icon::StyledIcon::build(icon)
|
||||
.add_class(name)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
StyledSelectChild::build()
|
||||
.add_class(name)
|
||||
.text(name)
|
||||
.icon(type_icon)
|
||||
.value(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub struct Model {
|
||||
pub priority: IssuePriority,
|
||||
pub description: Option<String>,
|
||||
pub description_text: Option<String>,
|
||||
pub estimate: Option<i32>,
|
||||
pub time_spent: Option<i32>,
|
||||
pub time_remaining: Option<i32>,
|
||||
pub project_id: Option<jirs_data::ProjectId>,
|
||||
pub user_ids: Vec<jirs_data::UserId>,
|
||||
pub reporter_id: Option<jirs_data::UserId>,
|
||||
pub issue_status_id: jirs_data::IssueStatusId,
|
||||
pub epic_id: Option<jirs_data::UserId>,
|
||||
|
||||
// modal fields
|
||||
pub title_state: StyledInputState,
|
||||
pub type_state: StyledSelectState,
|
||||
pub reporter_state: StyledSelectState,
|
||||
pub assignees_state: StyledSelectState,
|
||||
pub priority_state: StyledSelectState,
|
||||
|
||||
// epic
|
||||
pub epic_name_state: StyledSelectState,
|
||||
pub epic_starts_at_state: StyledDateTimeInputState,
|
||||
pub epic_ends_at_state: StyledDateTimeInputState,
|
||||
}
|
||||
|
||||
impl Default for Model {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
priority: Default::default(),
|
||||
description: Default::default(),
|
||||
description_text: Default::default(),
|
||||
estimate: Default::default(),
|
||||
time_spent: Default::default(),
|
||||
time_remaining: Default::default(),
|
||||
project_id: Default::default(),
|
||||
user_ids: Default::default(),
|
||||
reporter_id: Default::default(),
|
||||
issue_status_id: Default::default(),
|
||||
epic_id: Default::default(),
|
||||
title_state: StyledInputState::new(FieldId::AddIssueModal(IssueFieldId::Title), ""),
|
||||
type_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Type), vec![]),
|
||||
reporter_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::Reporter),
|
||||
vec![],
|
||||
),
|
||||
assignees_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::Assignees),
|
||||
vec![],
|
||||
),
|
||||
priority_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::Priority),
|
||||
vec![],
|
||||
),
|
||||
// epic
|
||||
epic_name_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::EpicName),
|
||||
vec![],
|
||||
),
|
||||
epic_starts_at_state: StyledDateTimeInputState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::EpicStartsAt),
|
||||
None,
|
||||
),
|
||||
epic_ends_at_state: StyledDateTimeInputState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::EpicEndsAt),
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IssueModal for Model {
|
||||
fn epic_id_value(&self) -> Option<u32> {
|
||||
self.epic_name_state.values.get(0).cloned()
|
||||
}
|
||||
|
||||
fn epic_state(&self) -> &StyledSelectState {
|
||||
&self.epic_name_state
|
||||
}
|
||||
|
||||
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||
self.title_state.update(msg);
|
||||
self.assignees_state.update(msg, orders);
|
||||
self.reporter_state.update(msg, orders);
|
||||
self.type_state.update(msg, orders);
|
||||
self.priority_state.update(msg, orders);
|
||||
self.epic_name_state.update(msg, orders);
|
||||
self.epic_starts_at_state.update(msg, orders);
|
||||
self.epic_ends_at_state.update(msg, orders);
|
||||
}
|
||||
}
|
122
jirs-client/src/modals/issues_create/update.rs
Normal file
122
jirs-client/src/modals/issues_create/update.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use {
|
||||
crate::{
|
||||
model::{IssueModal, ModalType},
|
||||
shared::styled_select::StyledSelectChanged,
|
||||
ws::send_ws_msg,
|
||||
FieldId, Msg, WebSocketChanged,
|
||||
},
|
||||
jirs_data::{IssueFieldId, UserId, WsMsg},
|
||||
seed::prelude::*,
|
||||
};
|
||||
|
||||
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 {
|
||||
ModalType::AddIssue(..) => true,
|
||||
_ => false,
|
||||
});
|
||||
let modal = match modal {
|
||||
Some(ModalType::AddIssue(modal)) => modal,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
modal.update_states(msg, orders);
|
||||
|
||||
match msg {
|
||||
Msg::AddEpic => {
|
||||
send_ws_msg(
|
||||
WsMsg::EpicCreate(modal.title_state.value.clone()),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::AddIssue => {
|
||||
let user_id = model.user.as_ref().map(|u| u.id).unwrap_or_default();
|
||||
let project_id = model.project.as_ref().map(|p| p.id).unwrap_or_default();
|
||||
let type_value = modal.type_state.values.get(0).cloned().unwrap_or_default();
|
||||
match type_value {
|
||||
0 | 1 | 2 => {
|
||||
let issue_type = type_value.into();
|
||||
let payload = jirs_data::CreateIssuePayload {
|
||||
title: modal.title_state.value.clone(),
|
||||
issue_type,
|
||||
issue_status_id: modal.issue_status_id,
|
||||
priority: modal.priority,
|
||||
description: modal.description.clone(),
|
||||
description_text: modal.description_text.clone(),
|
||||
estimate: modal.estimate,
|
||||
time_spent: modal.time_spent,
|
||||
time_remaining: modal.time_remaining,
|
||||
project_id: modal.project_id.unwrap_or(project_id),
|
||||
user_ids: modal.user_ids.clone(),
|
||||
reporter_id: modal.reporter_id.unwrap_or_else(|| user_id),
|
||||
epic_id: modal.epic_id,
|
||||
};
|
||||
|
||||
send_ws_msg(
|
||||
jirs_data::WsMsg::IssueCreate(payload),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
//
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueCreated(issue))) => {
|
||||
model.issues.push(issue.clone());
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::EpicCreated(_))) => {
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
Msg::StrInputChanged(FieldId::AddIssueModal(IssueFieldId::Description), value) => {
|
||||
modal.description = Some(value.clone());
|
||||
}
|
||||
|
||||
// ReporterAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AddIssueModal(IssueFieldId::Reporter),
|
||||
StyledSelectChanged::Changed(id),
|
||||
) => {
|
||||
modal.reporter_id = id.map(|n| n as UserId);
|
||||
}
|
||||
|
||||
// AssigneesAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AddIssueModal(IssueFieldId::Assignees),
|
||||
StyledSelectChanged::Changed(Some(id)),
|
||||
) => {
|
||||
let id = *id as UserId;
|
||||
if !modal.user_ids.contains(&id) {
|
||||
modal.user_ids.push(id);
|
||||
}
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AddIssueModal(IssueFieldId::Assignees),
|
||||
StyledSelectChanged::RemoveMulti(id),
|
||||
) => {
|
||||
let id = *id as i32;
|
||||
let mut old: Vec<i32> = vec![];
|
||||
std::mem::swap(&mut old, &mut modal.user_ids);
|
||||
for user_id in old {
|
||||
if id != user_id {
|
||||
modal.user_ids.push(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IssuePriorityAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AddIssueModal(IssueFieldId::Priority),
|
||||
StyledSelectChanged::Changed(Some(id)),
|
||||
) => {
|
||||
modal.priority = (*id).into();
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
@ -1,227 +1,21 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{IssueFieldId, IssuePriority, ToVec, UserId, WsMsg};
|
||||
|
||||
use crate::shared::styled_date_time_input::StyledDateTimeInput;
|
||||
use crate::{
|
||||
modal::issues::epic_field,
|
||||
model::{AddIssueModal, IssueModal, ModalType, Model},
|
||||
shared::{
|
||||
styled_button::StyledButton,
|
||||
styled_field::StyledField,
|
||||
styled_form::StyledForm,
|
||||
styled_input::StyledInput,
|
||||
styled_modal::{StyledModal, Variant as ModalVariant},
|
||||
styled_select::StyledSelect,
|
||||
styled_select::StyledSelectChanged,
|
||||
styled_select_child::{StyledSelectChild, StyledSelectChildBuilder},
|
||||
styled_textarea::StyledTextarea,
|
||||
ToChild, ToNode,
|
||||
use {
|
||||
crate::{
|
||||
modal::issues::epic_field,
|
||||
modals::issues_create::{Model as AddIssueModal, Type},
|
||||
model::Model,
|
||||
shared::{
|
||||
styled_button::StyledButton, styled_date_time_input::StyledDateTimeInput,
|
||||
styled_field::StyledField, styled_form::StyledForm, styled_input::StyledInput,
|
||||
styled_modal::StyledModal, styled_select::StyledSelect,
|
||||
styled_textarea::StyledTextarea, ToChild, ToNode,
|
||||
},
|
||||
FieldId, Msg,
|
||||
},
|
||||
ws::send_ws_msg,
|
||||
FieldId, Msg, WebSocketChanged,
|
||||
jirs_data::{IssueFieldId, IssuePriority, ToVec},
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Type {
|
||||
Task,
|
||||
Bug,
|
||||
Story,
|
||||
Epic,
|
||||
}
|
||||
|
||||
impl From<u32> for Type {
|
||||
fn from(n: u32) -> Self {
|
||||
match n {
|
||||
0 => Type::Task,
|
||||
1 => Type::Bug,
|
||||
2 => Type::Story,
|
||||
3 => Type::Epic,
|
||||
_ => Type::Task,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Type {
|
||||
fn ordered<'l>() -> &'l [Type] {
|
||||
use Type::*;
|
||||
&[Task, Bug, Story, Epic]
|
||||
}
|
||||
|
||||
fn submit_label(&self) -> &str {
|
||||
use Type::*;
|
||||
match self {
|
||||
Epic => "Create epic",
|
||||
Bug | Task | Story => "Create issue",
|
||||
}
|
||||
}
|
||||
|
||||
fn form_label(&self) -> &str {
|
||||
use Type::*;
|
||||
match self {
|
||||
Epic => "Create epic",
|
||||
Bug | Task | Story => "Create issue",
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_action(&self) -> Msg {
|
||||
use Type::*;
|
||||
match self {
|
||||
Epic => Msg::AddEpic,
|
||||
Bug | Task | Story => Msg::AddIssue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> ToChild<'l> for Type {
|
||||
type Builder = StyledSelectChildBuilder<'l>;
|
||||
|
||||
fn to_child<'m: 'l>(&'m self) -> Self::Builder {
|
||||
let name = match self {
|
||||
Type::Task => "Task",
|
||||
Type::Bug => "Bug",
|
||||
Type::Story => "Story",
|
||||
Type::Epic => "Epic",
|
||||
};
|
||||
let value = match self {
|
||||
Type::Task => 0,
|
||||
Type::Bug => 1,
|
||||
Type::Story => 2,
|
||||
Type::Epic => 3,
|
||||
};
|
||||
let icon = match self {
|
||||
Type::Task => crate::shared::styled_icon::Icon::Task,
|
||||
Type::Bug => crate::shared::styled_icon::Icon::Bug,
|
||||
Type::Story => crate::shared::styled_icon::Icon::Story,
|
||||
Type::Epic => crate::shared::styled_icon::Icon::Epic,
|
||||
};
|
||||
|
||||
let type_icon = crate::shared::styled_icon::StyledIcon::build(icon)
|
||||
.add_class(name)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
StyledSelectChild::build()
|
||||
.add_class(name)
|
||||
.text(name)
|
||||
.icon(type_icon)
|
||||
.value(value)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
ModalType::AddIssue(..) => true,
|
||||
_ => false,
|
||||
});
|
||||
let modal = match modal {
|
||||
Some(ModalType::AddIssue(modal)) => modal,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
modal.update_states(msg, orders);
|
||||
|
||||
match msg {
|
||||
Msg::AddEpic => {
|
||||
send_ws_msg(
|
||||
WsMsg::EpicCreate(modal.title_state.value.clone()),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::AddIssue => {
|
||||
let user_id = model.user.as_ref().map(|u| u.id).unwrap_or_default();
|
||||
let project_id = model.project.as_ref().map(|p| p.id).unwrap_or_default();
|
||||
let type_value = modal.type_state.values.get(0).cloned().unwrap_or_default();
|
||||
match type_value {
|
||||
0 | 1 | 2 => {
|
||||
let issue_type = type_value.into();
|
||||
let payload = jirs_data::CreateIssuePayload {
|
||||
title: modal.title_state.value.clone(),
|
||||
issue_type,
|
||||
issue_status_id: modal.issue_status_id,
|
||||
priority: modal.priority,
|
||||
description: modal.description.clone(),
|
||||
description_text: modal.description_text.clone(),
|
||||
estimate: modal.estimate,
|
||||
time_spent: modal.time_spent,
|
||||
time_remaining: modal.time_remaining,
|
||||
project_id: modal.project_id.unwrap_or(project_id),
|
||||
user_ids: modal.user_ids.clone(),
|
||||
reporter_id: modal.reporter_id.unwrap_or_else(|| user_id),
|
||||
epic_id: modal.epic_id,
|
||||
};
|
||||
|
||||
send_ws_msg(
|
||||
jirs_data::WsMsg::IssueCreate(payload),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
//
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueCreated(issue))) => {
|
||||
model.issues.push(issue.clone());
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::EpicCreated(_))) => {
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
Msg::StrInputChanged(FieldId::AddIssueModal(IssueFieldId::Description), value) => {
|
||||
modal.description = Some(value.clone());
|
||||
}
|
||||
|
||||
// ReporterAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AddIssueModal(IssueFieldId::Reporter),
|
||||
StyledSelectChanged::Changed(id),
|
||||
) => {
|
||||
modal.reporter_id = id.map(|n| n as UserId);
|
||||
}
|
||||
|
||||
// AssigneesAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AddIssueModal(IssueFieldId::Assignees),
|
||||
StyledSelectChanged::Changed(Some(id)),
|
||||
) => {
|
||||
let id = *id as UserId;
|
||||
if !modal.user_ids.contains(&id) {
|
||||
modal.user_ids.push(id);
|
||||
}
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AddIssueModal(IssueFieldId::Assignees),
|
||||
StyledSelectChanged::RemoveMulti(id),
|
||||
) => {
|
||||
let id = *id as i32;
|
||||
let mut old: Vec<i32> = vec![];
|
||||
std::mem::swap(&mut old, &mut modal.user_ids);
|
||||
for user_id in old {
|
||||
if id != user_id {
|
||||
modal.user_ids.push(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IssuePriorityAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AddIssueModal(IssueFieldId::Priority),
|
||||
StyledSelectChanged::Changed(Some(id)),
|
||||
) => {
|
||||
modal.priority = (*id).into();
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
pub fn view(model: &Model, modal: &Box<AddIssueModal>) -> Node<Msg> {
|
||||
let issue_type = modal
|
||||
.type_state
|
||||
.values
|
||||
@ -270,8 +64,11 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
let reporter_field = reporter_field(model, modal);
|
||||
let assignees_field = assignees_field(model, modal);
|
||||
let issue_priority_field = issue_priority_field(modal);
|
||||
let epic_field =
|
||||
epic_field(model, modal, FieldId::AddIssueModal(IssueFieldId::EpicName));
|
||||
let epic_field = epic_field(
|
||||
model,
|
||||
modal.as_ref(),
|
||||
FieldId::AddIssueModal(IssueFieldId::EpicName),
|
||||
);
|
||||
|
||||
form.add_field(short_summary_field)
|
||||
.add_field(description_field)
|
||||
@ -317,13 +114,13 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
StyledModal::build()
|
||||
.add_class("addIssue")
|
||||
.width(0)
|
||||
.variant(ModalVariant::Center)
|
||||
.variant(crate::shared::styled_modal::Variant::Center)
|
||||
.children(vec![form])
|
||||
.build()
|
||||
.into_node()
|
||||
}
|
||||
|
||||
fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
|
||||
fn issue_type_field(modal: &Box<AddIssueModal>) -> Node<Msg> {
|
||||
let select_type = StyledSelect::build()
|
||||
.name("type")
|
||||
.normal()
|
||||
@ -351,7 +148,7 @@ fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
|
||||
.into_node()
|
||||
}
|
||||
|
||||
fn short_summary_field(modal: &AddIssueModal) -> Node<Msg> {
|
||||
fn short_summary_field(modal: &Box<AddIssueModal>) -> Node<Msg> {
|
||||
let short_summary = StyledInput::build()
|
||||
.state(&modal.title_state)
|
||||
.build(FieldId::AddIssueModal(IssueFieldId::Title))
|
7
jirs-client/src/modals/issues_edit/mod.rs
Normal file
7
jirs-client/src/modals/issues_edit/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub use model::*;
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
mod model;
|
||||
mod update;
|
||||
mod view;
|
164
jirs-client/src/modals/issues_edit/model.rs
Normal file
164
jirs-client/src/modals/issues_edit/model.rs
Normal file
@ -0,0 +1,164 @@
|
||||
use {
|
||||
crate::{
|
||||
modal::time_tracking::value_for_time_tracking,
|
||||
model::{CommentForm, IssueModal},
|
||||
shared::{
|
||||
styled_date_time_input::StyledDateTimeInputState, styled_editor::Mode,
|
||||
styled_input::StyledInputState, styled_select::StyledSelectState,
|
||||
},
|
||||
EditIssueModalSection, FieldId, Msg,
|
||||
},
|
||||
jirs_data::{Issue, IssueFieldId, IssueId, TimeTracking, UpdateIssuePayload},
|
||||
seed::prelude::*,
|
||||
};
|
||||
|
||||
use crate::shared::styled_editor::StyledEditorState;
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub struct Model {
|
||||
pub id: IssueId,
|
||||
pub link_copied: bool,
|
||||
pub payload: UpdateIssuePayload,
|
||||
pub top_type_state: StyledSelectState,
|
||||
pub status_state: StyledSelectState,
|
||||
pub reporter_state: StyledSelectState,
|
||||
pub assignees_state: StyledSelectState,
|
||||
pub priority_state: StyledSelectState,
|
||||
pub epic_name_state: StyledSelectState,
|
||||
pub epic_starts_at_state: StyledDateTimeInputState,
|
||||
pub epic_ends_at_state: StyledDateTimeInputState,
|
||||
|
||||
pub estimate: StyledInputState,
|
||||
pub estimate_select: StyledSelectState,
|
||||
pub time_spent: StyledInputState,
|
||||
pub time_spent_select: StyledSelectState,
|
||||
pub time_remaining: StyledInputState,
|
||||
pub time_remaining_select: StyledSelectState,
|
||||
|
||||
pub description_state: StyledEditorState,
|
||||
|
||||
// comments
|
||||
pub comment_form: CommentForm,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn new(issue: &Issue, time_tracking_type: TimeTracking) -> Self {
|
||||
Self {
|
||||
id: issue.id,
|
||||
link_copied: false,
|
||||
payload: UpdateIssuePayload {
|
||||
title: issue.title.clone(),
|
||||
issue_type: issue.issue_type,
|
||||
issue_status_id: issue.issue_status_id,
|
||||
priority: issue.priority,
|
||||
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(),
|
||||
},
|
||||
top_type_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
||||
issue.estimate.map(|v| vec![v as u32]).unwrap_or_default(),
|
||||
),
|
||||
status_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
||||
vec![issue.issue_status_id as u32],
|
||||
),
|
||||
reporter_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
||||
vec![issue.reporter_id as u32],
|
||||
),
|
||||
assignees_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||
issue.user_ids.iter().map(|n| *n as u32).collect(),
|
||||
),
|
||||
priority_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
||||
vec![issue.priority.into()],
|
||||
),
|
||||
estimate: StyledInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
value_for_time_tracking(&issue.estimate, &time_tracking_type),
|
||||
),
|
||||
estimate_select: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
issue.estimate.map(|n| vec![n as u32]).unwrap_or_default(),
|
||||
),
|
||||
time_spent: StyledInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
|
||||
value_for_time_tracking(&issue.time_spent, &time_tracking_type),
|
||||
),
|
||||
time_spent_select: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
|
||||
issue.time_spent.map(|n| vec![n as u32]).unwrap_or_default(),
|
||||
),
|
||||
time_remaining: StyledInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
|
||||
value_for_time_tracking(&issue.time_remaining, &time_tracking_type),
|
||||
),
|
||||
time_remaining_select: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
|
||||
issue
|
||||
.time_remaining
|
||||
.map(|n| vec![n as u32])
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
description_state: StyledEditorState::new(
|
||||
Mode::View,
|
||||
issue.description_text.as_deref().unwrap_or(""),
|
||||
),
|
||||
comment_form: CommentForm {
|
||||
id: None,
|
||||
body: String::new(),
|
||||
creating: false,
|
||||
},
|
||||
// epic
|
||||
epic_name_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||
issue
|
||||
.epic_id
|
||||
.as_ref()
|
||||
.map(|id| vec![*id as u32])
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
epic_starts_at_state: StyledDateTimeInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt)),
|
||||
None,
|
||||
),
|
||||
epic_ends_at_state: StyledDateTimeInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt)),
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IssueModal for Model {
|
||||
fn epic_id_value(&self) -> Option<u32> {
|
||||
self.epic_name_state.values.get(0).cloned()
|
||||
}
|
||||
|
||||
fn epic_state(&self) -> &StyledSelectState {
|
||||
&self.epic_name_state
|
||||
}
|
||||
|
||||
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||
self.top_type_state.update(msg, orders);
|
||||
self.status_state.update(msg, orders);
|
||||
self.reporter_state.update(msg, orders);
|
||||
self.assignees_state.update(msg, orders);
|
||||
self.priority_state.update(msg, orders);
|
||||
self.estimate.update(msg);
|
||||
self.estimate_select.update(msg, orders);
|
||||
self.time_spent.update(msg);
|
||||
self.time_spent_select.update(msg, orders);
|
||||
self.time_remaining.update(msg);
|
||||
self.time_remaining_select.update(msg, orders);
|
||||
self.epic_name_state.update(msg, orders);
|
||||
}
|
||||
}
|
347
jirs-client/src/modals/issues_edit/update.rs
Normal file
347
jirs-client/src/modals/issues_edit/update.rs
Normal file
@ -0,0 +1,347 @@
|
||||
use {
|
||||
crate::{
|
||||
modals::issues_edit::Model as EditIssueModal,
|
||||
model::{IssueModal, ModalType, Model},
|
||||
shared::styled_select::StyledSelectChanged,
|
||||
ws::send_ws_msg,
|
||||
EditIssueModalSection, FieldChange, FieldId, Msg, OperationKind, ResourceKind,
|
||||
},
|
||||
jirs_data::*,
|
||||
seed::prelude::*,
|
||||
};
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
let modal: &mut EditIssueModal = match model.modals.get_mut(0) {
|
||||
Some(ModalType::EditIssue(_issue_id, modal)) => modal,
|
||||
_ => return,
|
||||
};
|
||||
modal.update_states(msg, orders);
|
||||
|
||||
match msg {
|
||||
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleModified, Some(id)) => {
|
||||
if let Some(issue) = model.issues_by_id.get(id) {
|
||||
modal.payload = issue.clone().into();
|
||||
}
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.issue_type = (*value).into();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Type,
|
||||
PayloadVariant::IssueType(modal.payload.issue_type),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.issue_status_id = *value as IssueStatusId;
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::IssueStatusId,
|
||||
PayloadVariant::I32(modal.payload.issue_status_id),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.reporter_id = *value as i32;
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Reporter,
|
||||
PayloadVariant::I32(modal.payload.reporter_id),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.user_ids.push(*value as i32);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Assignees,
|
||||
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||
StyledSelectChanged::RemoveMulti(value),
|
||||
) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut modal.payload.user_ids);
|
||||
let dropped = *value as i32;
|
||||
for id in old.into_iter() {
|
||||
if id != dropped {
|
||||
modal.payload.user_ids.push(id);
|
||||
}
|
||||
}
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Assignees,
|
||||
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
) => {
|
||||
modal.payload.priority = (*value).into();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Priority,
|
||||
PayloadVariant::IssuePriority(modal.payload.priority),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
|
||||
value,
|
||||
) => {
|
||||
modal.payload.title = value.clone();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Title,
|
||||
PayloadVariant::String(modal.payload.title.clone()),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
|
||||
value,
|
||||
) => {
|
||||
// modal.payload.description = Some(value.clone());
|
||||
modal.payload.description_text = Some(value.clone());
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Description,
|
||||
PayloadVariant::String(
|
||||
modal
|
||||
.payload
|
||||
.description
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
orders.skip();
|
||||
}
|
||||
// TimeSpent
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
|
||||
..,
|
||||
) => {
|
||||
modal.payload.time_spent = modal.time_spent.represent_f64_as_i32();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::TimeSpent,
|
||||
PayloadVariant::OptionI32(modal.payload.time_spent),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
|
||||
StyledSelectChanged::Changed(..),
|
||||
) => {
|
||||
modal.payload.time_spent = modal.time_spent_select.values.get(0).map(|n| *n as i32);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::TimeSpent,
|
||||
PayloadVariant::OptionI32(modal.payload.time_spent),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
// Time Remaining
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
|
||||
..,
|
||||
) => {
|
||||
modal.payload.time_remaining = modal.time_remaining.represent_f64_as_i32();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::TimeRemaining,
|
||||
PayloadVariant::OptionI32(modal.payload.time_remaining),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
|
||||
StyledSelectChanged::Changed(..),
|
||||
) => {
|
||||
modal.payload.time_remaining =
|
||||
modal.time_remaining_select.values.get(0).map(|n| *n as i32);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::TimeRemaining,
|
||||
PayloadVariant::OptionI32(modal.payload.time_remaining),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
// Estimate
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
..,
|
||||
) => {
|
||||
modal.payload.estimate = modal.estimate.represent_f64_as_i32();
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Estimate,
|
||||
PayloadVariant::OptionI32(modal.payload.estimate),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
StyledSelectChanged::Changed(..),
|
||||
) => {
|
||||
modal.payload.estimate = modal.estimate_select.values.get(0).map(|n| *n as i32);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::Estimate,
|
||||
PayloadVariant::OptionI32(modal.payload.estimate),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||
StyledSelectChanged::Changed(v),
|
||||
) => {
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
modal.id,
|
||||
IssueFieldId::EpicName,
|
||||
PayloadVariant::OptionI32(v.map(|n| n as EpicId)),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::ModalChanged(FieldChange::TabChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
|
||||
mode,
|
||||
)) => {
|
||||
modal.description_state.mode = mode.clone();
|
||||
}
|
||||
Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
flag,
|
||||
)) => {
|
||||
modal.comment_form.creating = *flag;
|
||||
if !*flag {
|
||||
modal.comment_form.body.clear();
|
||||
modal.comment_form.id = None;
|
||||
}
|
||||
}
|
||||
//
|
||||
// comments
|
||||
//
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
text,
|
||||
) => {
|
||||
modal.comment_form.body = text.clone();
|
||||
}
|
||||
Msg::SaveComment => {
|
||||
let msg = match modal.comment_form.id {
|
||||
Some(id) => WsMsg::CommentUpdate(UpdateCommentPayload {
|
||||
id,
|
||||
body: modal.comment_form.body.clone(),
|
||||
}),
|
||||
_ => WsMsg::CommentCreate(CreateCommentPayload {
|
||||
user_id: None,
|
||||
body: modal.comment_form.body.clone(),
|
||||
issue_id: modal.id,
|
||||
}),
|
||||
};
|
||||
send_ws_msg(msg, model.ws.as_ref(), orders);
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
false,
|
||||
)));
|
||||
}
|
||||
Msg::ModalChanged(FieldChange::EditComment(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
comment_id,
|
||||
)) => {
|
||||
let id = *comment_id;
|
||||
let body = model
|
||||
.comments
|
||||
.iter()
|
||||
.find(|c| c.id == id)
|
||||
.map(|c| c.body.clone())
|
||||
.unwrap_or_default();
|
||||
modal.comment_form.body = body;
|
||||
modal.comment_form.id = Some(id);
|
||||
modal.comment_form.creating = true;
|
||||
}
|
||||
Msg::DeleteComment(comment_id) => {
|
||||
send_ws_msg(WsMsg::CommentDelete(*comment_id), model.ws.as_ref(), orders);
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
// global
|
||||
Msg::GlobalKeyDown { key, .. } if key.as_str() == "m" && !modal.comment_form.creating => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
true,
|
||||
)));
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
109
jirs-client/src/modals/issues_edit/view/comments.rs
Normal file
109
jirs-client/src/modals/issues_edit/view/comments.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use {
|
||||
crate::{
|
||||
modals::issues_edit::Model as EditIssueModal,
|
||||
model::{CommentForm, ModalType, Model},
|
||||
shared::{
|
||||
styled_avatar::StyledAvatar, styled_button::StyledButton,
|
||||
styled_textarea::StyledTextarea, ToNode,
|
||||
},
|
||||
EditIssueModalSection, FieldChange, FieldId, Msg,
|
||||
},
|
||||
jirs_data::{Comment, CommentFieldId},
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
pub fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
|
||||
let submit_comment_form = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::SaveComment
|
||||
});
|
||||
let close_comment_form = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
false,
|
||||
))
|
||||
});
|
||||
|
||||
let text_area = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Comment(
|
||||
CommentFieldId::Body,
|
||||
)))
|
||||
.value(form.body.as_str())
|
||||
.placeholder("Add a comment...")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let submit = StyledButton::build()
|
||||
.primary()
|
||||
.on_click(submit_comment_form)
|
||||
.text("Save")
|
||||
.build()
|
||||
.into_node();
|
||||
let cancel = StyledButton::build()
|
||||
.empty()
|
||||
.on_click(close_comment_form)
|
||||
.text("Cancel")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
vec![text_area, div![C!["actions"], submit, cancel]]
|
||||
}
|
||||
|
||||
pub fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Option<Node<Msg>> {
|
||||
let show_form = modal.comment_form.creating && modal.comment_form.id == Some(comment.id);
|
||||
|
||||
let user = model.users_by_id.get(&comment.user_id)?;
|
||||
|
||||
let avatar = StyledAvatar::build()
|
||||
.size(32)
|
||||
.avatar_url(user.avatar_url.as_deref()?)
|
||||
.add_class("userAvatar")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let buttons = if model.user.as_ref().map(|u| u.id) == Some(comment.user_id) {
|
||||
let comment_id = comment.id;
|
||||
let delete_comment_handler = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalOpened(Box::new(ModalType::DeleteCommentConfirm(comment_id)))
|
||||
});
|
||||
let edit_button = StyledButton::build()
|
||||
.add_class("editButton")
|
||||
.on_click(mouse_ev(Ev::Click, move |_| {
|
||||
Msg::ModalChanged(FieldChange::EditComment(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
comment_id,
|
||||
))
|
||||
}))
|
||||
.text("Edit")
|
||||
.empty()
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let cancel_button = StyledButton::build()
|
||||
.add_class("deleteButton")
|
||||
.on_click(delete_comment_handler)
|
||||
.text("Delete")
|
||||
.empty()
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
vec![edit_button, cancel_button]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let content = if show_form {
|
||||
div![C!["content"], build_comment_form(&modal.comment_form)]
|
||||
} else {
|
||||
div![
|
||||
C!["content"],
|
||||
div![C!["userName"], user.name.as_str()],
|
||||
p![C!["body"], comment.body.as_str()],
|
||||
buttons,
|
||||
]
|
||||
};
|
||||
|
||||
let node = div![C!["styledComment"], avatar, content];
|
||||
Some(node)
|
||||
}
|
397
jirs-client/src/modals/issues_edit/view/mod.rs
Normal file
397
jirs-client/src/modals/issues_edit/view/mod.rs
Normal file
@ -0,0 +1,397 @@
|
||||
use {
|
||||
crate::{
|
||||
modal::{issues::epic_field, time_tracking::time_tracking_field},
|
||||
modals::issues_edit::Model as EditIssueModal,
|
||||
model::{ModalType, Model},
|
||||
shared::{
|
||||
styled_avatar::StyledAvatar, styled_button::StyledButton, styled_editor::StyledEditor,
|
||||
styled_field::StyledField, styled_icon::Icon, styled_input::StyledInput,
|
||||
styled_select::StyledSelect, tracking_widget::tracking_link, ToChild, ToNode,
|
||||
},
|
||||
EditIssueModalSection, FieldChange, FieldId, Msg,
|
||||
},
|
||||
comments::*,
|
||||
jirs_data::{CommentFieldId, IssueFieldId, IssuePriority, IssueType, TimeTracking, ToVec},
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
mod comments;
|
||||
|
||||
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
div![
|
||||
C!["issueDetails"],
|
||||
modal_header(model, modal),
|
||||
div![
|
||||
C!["content"],
|
||||
left_modal_column(model, modal),
|
||||
right_modal_column(model, modal),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
id,
|
||||
payload,
|
||||
top_type_state,
|
||||
link_copied,
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let issue_id = *id;
|
||||
|
||||
let click_handler = mouse_ev(Ev::Click, move |_| {
|
||||
let link = format!("http://localhost:7000/issues/{id}", id = issue_id);
|
||||
let el = match seed::html_document().create_element("textarea") {
|
||||
Ok(el) => el
|
||||
.dyn_ref::<web_sys::HtmlTextAreaElement>()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
_ => return None as Option<Msg>,
|
||||
};
|
||||
seed::body().append_child(&el).unwrap();
|
||||
el.set_text_content(Some(link.as_str()));
|
||||
el.select();
|
||||
el.set_selection_range(0, 9999).unwrap();
|
||||
seed::html_document().exec_command("copy").unwrap();
|
||||
seed::body().remove_child(&el).unwrap();
|
||||
Some(Msg::ModalChanged(FieldChange::LinkCopied(
|
||||
FieldId::CopyButtonLabel,
|
||||
true,
|
||||
)))
|
||||
});
|
||||
let close_handler = mouse_ev(Ev::Click, |ev| {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
seed::Url::new().add_path_part("board").go_and_push();
|
||||
|
||||
Msg::ModalDropped
|
||||
});
|
||||
let delete_confirmation_handler = mouse_ev(Ev::Click, move |_| {
|
||||
Msg::ModalOpened(Box::new(ModalType::DeleteIssueConfirm(issue_id)))
|
||||
});
|
||||
|
||||
let copy_button = StyledButton::build()
|
||||
.empty()
|
||||
.icon(Icon::Link)
|
||||
.on_click(click_handler)
|
||||
.children(vec![span![if *link_copied {
|
||||
"Link Copied"
|
||||
} else {
|
||||
"Copy link"
|
||||
}]])
|
||||
.build()
|
||||
.into_node();
|
||||
let delete_button = StyledButton::build()
|
||||
.empty()
|
||||
.icon(Icon::Trash.into_styled_builder().size(19).build())
|
||||
.on_click(delete_confirmation_handler)
|
||||
.build()
|
||||
.into_node();
|
||||
let close_button = StyledButton::build()
|
||||
.empty()
|
||||
.icon(Icon::Close.into_styled_builder().size(24).build())
|
||||
.on_click(close_handler)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let issue_types = IssueType::ordered();
|
||||
let issue_type_select = StyledSelect::build()
|
||||
.dropdown_width(150)
|
||||
.name("type")
|
||||
.text_filter(top_type_state.text_filter.as_str())
|
||||
.opened(top_type_state.opened)
|
||||
.valid(true)
|
||||
.options(
|
||||
issue_types
|
||||
.iter()
|
||||
.map(|t| t.to_child().name("type"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![{
|
||||
let id = modal.id;
|
||||
let issue_type = &payload.issue_type;
|
||||
issue_type
|
||||
.to_child()
|
||||
.name("type")
|
||||
.text_owned(format!("{} - {}", issue_type, id))
|
||||
}])
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Type,
|
||||
)))
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
C!["topActions"],
|
||||
issue_type_select,
|
||||
div![
|
||||
C!["topActionsRight"],
|
||||
copy_button,
|
||||
delete_button,
|
||||
close_button
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
payload,
|
||||
description_state,
|
||||
comment_form,
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let title = StyledInput::build()
|
||||
.add_input_class("issueSummary")
|
||||
.add_wrapper_class("issueSummary")
|
||||
.add_wrapper_class("textarea")
|
||||
.value(payload.title.as_str())
|
||||
.valid(payload.title.len() >= 3)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Title,
|
||||
)))
|
||||
.into_node();
|
||||
|
||||
let description = {
|
||||
StyledEditor::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Description,
|
||||
)))
|
||||
.initial_text(description_state.initial_text.as_str())
|
||||
.html(payload.description.as_ref().cloned().unwrap_or_default())
|
||||
.mode(description_state.mode.clone())
|
||||
.update_on(Ev::Change)
|
||||
.build()
|
||||
.into_node()
|
||||
};
|
||||
let description_field = StyledField::build().input(description).build().into_node();
|
||||
|
||||
let user_avatar = StyledAvatar::build()
|
||||
.add_class("userAvatar")
|
||||
.size(32)
|
||||
.avatar_url(
|
||||
model
|
||||
.user
|
||||
.as_ref()
|
||||
.and_then(|u| u.avatar_url.as_deref())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let create_comment = if comment_form.creating && comment_form.id.is_none() {
|
||||
build_comment_form(comment_form)
|
||||
} else {
|
||||
let creating_comment = comment_form.creating;
|
||||
let handler = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::ToggleCommentForm(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
|
||||
!creating_comment,
|
||||
))
|
||||
});
|
||||
vec![div![C!["fakeTextArea"], "Add a comment...", handler]]
|
||||
};
|
||||
|
||||
let comments: Vec<Node<Msg>> = model
|
||||
.comments
|
||||
.iter()
|
||||
.flat_map(|c| comment(model, modal, c))
|
||||
.collect();
|
||||
|
||||
div![
|
||||
C!["left"],
|
||||
title,
|
||||
description_field,
|
||||
div![
|
||||
C!["comments"],
|
||||
div![C!["title"], "Comments"],
|
||||
div![
|
||||
C!["create"],
|
||||
user_avatar,
|
||||
div![
|
||||
C!["right"],
|
||||
create_comment,
|
||||
div![
|
||||
C!["proTip"],
|
||||
strong![C!["strong"], "Pro tip: "],
|
||||
"press ",
|
||||
span![C!["tipLetter"], "M"],
|
||||
" to comment"
|
||||
]
|
||||
]
|
||||
],
|
||||
comments
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
payload,
|
||||
status_state,
|
||||
reporter_state,
|
||||
assignees_state,
|
||||
priority_state,
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let status = StyledSelect::build()
|
||||
.name("status")
|
||||
.opened(status_state.opened)
|
||||
.normal()
|
||||
.text_filter(status_state.text_filter.as_str())
|
||||
.options(
|
||||
model
|
||||
.issue_statuses
|
||||
.iter()
|
||||
.map(|opt| opt.to_child().name("status"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.issue_statuses
|
||||
.iter()
|
||||
.filter(|is| is.id == payload.issue_status_id)
|
||||
.map(|is| is.to_child().name("status"))
|
||||
.collect(),
|
||||
)
|
||||
.valid(true)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::IssueStatusId,
|
||||
)))
|
||||
.into_node();
|
||||
let status_field = StyledField::build()
|
||||
.input(status)
|
||||
.label("Status")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let assignees = StyledSelect::build()
|
||||
.name("assignees")
|
||||
.opened(assignees_state.opened)
|
||||
.empty()
|
||||
.multi()
|
||||
.text_filter(assignees_state.text_filter.as_str())
|
||||
.options(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| user.to_child().name("assignees"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.filter(|user| payload.user_ids.contains(&user.id))
|
||||
.map(|user| user.to_child().name("assignees"))
|
||||
.collect(),
|
||||
)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Assignees,
|
||||
)))
|
||||
.into_node();
|
||||
let assignees_field = StyledField::build()
|
||||
.input(assignees)
|
||||
.label("Assignees")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let reporter = StyledSelect::build()
|
||||
.name("reporter")
|
||||
.opened(reporter_state.opened)
|
||||
.empty()
|
||||
.text_filter(reporter_state.text_filter.as_str())
|
||||
.options(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| user.to_child().name("reporter"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.filter(|user| payload.reporter_id == user.id)
|
||||
.map(|user| user.to_child().name("reporter"))
|
||||
.collect(),
|
||||
)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Reporter,
|
||||
)))
|
||||
.into_node();
|
||||
let reporter_field = StyledField::build()
|
||||
.input(reporter)
|
||||
.label("Reporter")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let issue_priorities = IssuePriority::ordered();
|
||||
let priority = StyledSelect::build()
|
||||
.name("priority")
|
||||
.opened(priority_state.opened)
|
||||
.empty()
|
||||
.text_filter(priority_state.text_filter.as_str())
|
||||
.options(
|
||||
issue_priorities
|
||||
.iter()
|
||||
.map(|p| p.to_child().name("priority"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![payload.priority.to_child().name("priority")])
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Priority,
|
||||
)))
|
||||
.into_node();
|
||||
let priority_field = StyledField::build()
|
||||
.input(priority)
|
||||
.label("Priority")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let time_tracking_type = model
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|p| p.time_tracking)
|
||||
.unwrap_or_else(|| TimeTracking::Untracked);
|
||||
|
||||
let (estimate_field, tracking_field) = if time_tracking_type != TimeTracking::Untracked {
|
||||
let estimate_field = time_tracking_field(
|
||||
time_tracking_type,
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
"Original Estimate (hours)",
|
||||
&modal.estimate,
|
||||
&modal.estimate_select,
|
||||
);
|
||||
|
||||
let tracking = tracking_link(model, modal);
|
||||
let tracking_field = StyledField::build()
|
||||
.label("TIME TRACKING")
|
||||
.tip("")
|
||||
.input(tracking)
|
||||
.build()
|
||||
.into_node();
|
||||
(estimate_field, tracking_field)
|
||||
} else {
|
||||
(Node::Empty, Node::Empty)
|
||||
};
|
||||
|
||||
let epic_field = epic_field(
|
||||
model,
|
||||
modal,
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||
)
|
||||
.unwrap_or(Node::Empty);
|
||||
|
||||
div![
|
||||
C!["right"],
|
||||
status_field,
|
||||
assignees_field,
|
||||
reporter_field,
|
||||
priority_field,
|
||||
estimate_field,
|
||||
tracking_field,
|
||||
epic_field,
|
||||
]
|
||||
}
|
3
jirs-client/src/modals/mod.rs
Normal file
3
jirs-client/src/modals/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod issue_statuses_delete;
|
||||
pub mod issues_create;
|
||||
pub mod issues_edit;
|
@ -1,26 +1,24 @@
|
||||
use std::collections::hash_map::HashMap;
|
||||
|
||||
use chrono::{prelude::*, NaiveDate};
|
||||
use seed::app::Orders;
|
||||
use seed::browser::web_socket::WebSocket;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::{
|
||||
modal::time_tracking::value_for_time_tracking,
|
||||
shared::{
|
||||
drag::DragState, styled_checkbox::StyledCheckboxState,
|
||||
styled_date_time_input::StyledDateTimeInputState, styled_editor::Mode,
|
||||
styled_image_input::StyledImageInputState, styled_input::StyledInputState,
|
||||
/*styled_rte::StyledRteState,*/ styled_select::StyledSelectState,
|
||||
use {
|
||||
crate::{
|
||||
pages::{
|
||||
invite_page::InvitePage, profile_page::model::ProfilePage,
|
||||
project_page::model::ProjectPage, project_settings_page::ProjectSettingsPage,
|
||||
reports_page::model::ReportsPage, sign_in_page::model::SignInPage,
|
||||
sign_up_page::model::SignUpPage, users_page::model::UsersPage,
|
||||
},
|
||||
shared::styled_select::StyledSelectState,
|
||||
Msg,
|
||||
},
|
||||
EditIssueModalSection, FieldId, Msg, ProjectFieldId,
|
||||
jirs_data::*,
|
||||
seed::{app::Orders, browser::web_socket::WebSocket},
|
||||
serde::{Deserialize, Serialize},
|
||||
std::collections::hash_map::HashMap,
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
pub trait IssueModal {
|
||||
fn epic_id_value(&self) -> Option<u32>;
|
||||
|
||||
fn epic_state(&self) -> &StyledSelectState;
|
||||
|
||||
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>);
|
||||
@ -28,12 +26,12 @@ pub trait IssueModal {
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub enum ModalType {
|
||||
AddIssue(Box<AddIssueModal>),
|
||||
EditIssue(IssueId, Box<EditIssueModal>),
|
||||
AddIssue(Box<crate::modals::issues_create::Model>),
|
||||
EditIssue(IssueId, Box<crate::modals::issues_edit::Model>),
|
||||
DeleteIssueConfirm(IssueId),
|
||||
DeleteCommentConfirm(CommentId),
|
||||
TimeTracking(IssueId),
|
||||
DeleteIssueStatusModal(Box<DeleteIssueStatusModal>),
|
||||
DeleteIssueStatusModal(Box<crate::modals::issue_statuses_delete::Model>),
|
||||
#[cfg(debug_assertions)]
|
||||
DebugModal,
|
||||
}
|
||||
@ -45,259 +43,6 @@ pub struct CommentForm {
|
||||
pub creating: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub struct DeleteIssueStatusModal {
|
||||
pub delete_id: IssueStatusId,
|
||||
pub receiver_id: Option<IssueStatusId>,
|
||||
}
|
||||
|
||||
impl DeleteIssueStatusModal {
|
||||
pub fn new(delete_id: IssueStatusId) -> Self {
|
||||
Self {
|
||||
delete_id,
|
||||
receiver_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub struct EditIssueModal {
|
||||
pub id: IssueId,
|
||||
pub link_copied: bool,
|
||||
pub payload: UpdateIssuePayload,
|
||||
pub top_type_state: StyledSelectState,
|
||||
pub status_state: StyledSelectState,
|
||||
pub reporter_state: StyledSelectState,
|
||||
pub assignees_state: StyledSelectState,
|
||||
pub priority_state: StyledSelectState,
|
||||
pub epic_name_state: StyledSelectState,
|
||||
pub epic_starts_at_state: StyledDateTimeInputState,
|
||||
pub epic_ends_at_state: StyledDateTimeInputState,
|
||||
|
||||
pub estimate: StyledInputState,
|
||||
pub estimate_select: StyledSelectState,
|
||||
pub time_spent: StyledInputState,
|
||||
pub time_spent_select: StyledSelectState,
|
||||
pub time_remaining: StyledInputState,
|
||||
pub time_remaining_select: StyledSelectState,
|
||||
|
||||
pub description_editor_mode: Mode,
|
||||
|
||||
// comments
|
||||
pub comment_form: CommentForm,
|
||||
}
|
||||
|
||||
impl IssueModal for EditIssueModal {
|
||||
fn epic_id_value(&self) -> Option<u32> {
|
||||
self.epic_name_state.values.get(0).cloned()
|
||||
}
|
||||
|
||||
fn epic_state(&self) -> &StyledSelectState {
|
||||
&self.epic_name_state
|
||||
}
|
||||
|
||||
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||
self.top_type_state.update(msg, orders);
|
||||
self.status_state.update(msg, orders);
|
||||
self.reporter_state.update(msg, orders);
|
||||
self.assignees_state.update(msg, orders);
|
||||
self.priority_state.update(msg, orders);
|
||||
self.estimate.update(msg);
|
||||
self.estimate_select.update(msg, orders);
|
||||
self.time_spent.update(msg);
|
||||
self.time_spent_select.update(msg, orders);
|
||||
self.time_remaining.update(msg);
|
||||
self.time_remaining_select.update(msg, orders);
|
||||
self.epic_name_state.update(msg, orders);
|
||||
}
|
||||
}
|
||||
|
||||
impl EditIssueModal {
|
||||
pub fn new(issue: &Issue, time_tracking_type: TimeTracking) -> Self {
|
||||
Self {
|
||||
id: issue.id,
|
||||
link_copied: false,
|
||||
payload: UpdateIssuePayload {
|
||||
title: issue.title.clone(),
|
||||
issue_type: issue.issue_type,
|
||||
issue_status_id: issue.issue_status_id,
|
||||
priority: issue.priority,
|
||||
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(),
|
||||
},
|
||||
top_type_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
||||
issue.estimate.map(|v| vec![v as u32]).unwrap_or_default(),
|
||||
),
|
||||
status_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
||||
vec![issue.issue_status_id as u32],
|
||||
),
|
||||
reporter_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
||||
vec![issue.reporter_id as u32],
|
||||
),
|
||||
assignees_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||
issue.user_ids.iter().map(|n| *n as u32).collect(),
|
||||
),
|
||||
priority_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
||||
vec![issue.priority.into()],
|
||||
),
|
||||
estimate: StyledInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
value_for_time_tracking(&issue.estimate, &time_tracking_type),
|
||||
),
|
||||
estimate_select: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||
issue.estimate.map(|n| vec![n as u32]).unwrap_or_default(),
|
||||
),
|
||||
time_spent: StyledInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
|
||||
value_for_time_tracking(&issue.time_spent, &time_tracking_type),
|
||||
),
|
||||
time_spent_select: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
|
||||
issue.time_spent.map(|n| vec![n as u32]).unwrap_or_default(),
|
||||
),
|
||||
time_remaining: StyledInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
|
||||
value_for_time_tracking(&issue.time_remaining, &time_tracking_type),
|
||||
),
|
||||
time_remaining_select: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
|
||||
issue
|
||||
.time_remaining
|
||||
.map(|n| vec![n as u32])
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
description_editor_mode: Mode::View,
|
||||
comment_form: CommentForm {
|
||||
id: None,
|
||||
body: String::new(),
|
||||
creating: false,
|
||||
},
|
||||
// epic
|
||||
epic_name_state: StyledSelectState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||
issue
|
||||
.epic_id
|
||||
.as_ref()
|
||||
.map(|id| vec![*id as u32])
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
epic_starts_at_state: StyledDateTimeInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt)),
|
||||
None,
|
||||
),
|
||||
epic_ends_at_state: StyledDateTimeInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt)),
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub struct AddIssueModal {
|
||||
pub priority: IssuePriority,
|
||||
pub description: Option<String>,
|
||||
pub description_text: Option<String>,
|
||||
pub estimate: Option<i32>,
|
||||
pub time_spent: Option<i32>,
|
||||
pub time_remaining: Option<i32>,
|
||||
pub project_id: Option<jirs_data::ProjectId>,
|
||||
pub user_ids: Vec<jirs_data::UserId>,
|
||||
pub reporter_id: Option<jirs_data::UserId>,
|
||||
pub issue_status_id: jirs_data::IssueStatusId,
|
||||
pub epic_id: Option<jirs_data::UserId>,
|
||||
|
||||
// modal fields
|
||||
pub title_state: StyledInputState,
|
||||
pub type_state: StyledSelectState,
|
||||
pub reporter_state: StyledSelectState,
|
||||
pub assignees_state: StyledSelectState,
|
||||
pub priority_state: StyledSelectState,
|
||||
// epic
|
||||
pub epic_name_state: StyledSelectState,
|
||||
pub epic_starts_at_state: StyledDateTimeInputState,
|
||||
pub epic_ends_at_state: StyledDateTimeInputState,
|
||||
}
|
||||
|
||||
impl IssueModal for AddIssueModal {
|
||||
fn epic_id_value(&self) -> Option<u32> {
|
||||
self.epic_name_state.values.get(0).cloned()
|
||||
}
|
||||
|
||||
fn epic_state(&self) -> &StyledSelectState {
|
||||
&self.epic_name_state
|
||||
}
|
||||
|
||||
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||
self.title_state.update(msg);
|
||||
self.assignees_state.update(msg, orders);
|
||||
self.reporter_state.update(msg, orders);
|
||||
self.type_state.update(msg, orders);
|
||||
self.priority_state.update(msg, orders);
|
||||
self.epic_name_state.update(msg, orders);
|
||||
self.epic_starts_at_state.update(msg, orders);
|
||||
self.epic_ends_at_state.update(msg, orders);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AddIssueModal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
priority: Default::default(),
|
||||
description: Default::default(),
|
||||
description_text: Default::default(),
|
||||
estimate: Default::default(),
|
||||
time_spent: Default::default(),
|
||||
time_remaining: Default::default(),
|
||||
project_id: Default::default(),
|
||||
user_ids: Default::default(),
|
||||
reporter_id: Default::default(),
|
||||
issue_status_id: Default::default(),
|
||||
epic_id: Default::default(),
|
||||
title_state: StyledInputState::new(FieldId::AddIssueModal(IssueFieldId::Title), ""),
|
||||
type_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Type), vec![]),
|
||||
reporter_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::Reporter),
|
||||
vec![],
|
||||
),
|
||||
assignees_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::Assignees),
|
||||
vec![],
|
||||
),
|
||||
priority_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::Priority),
|
||||
vec![],
|
||||
),
|
||||
// epic
|
||||
epic_name_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::EpicName),
|
||||
vec![],
|
||||
),
|
||||
epic_starts_at_state: StyledDateTimeInputState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::EpicStartsAt),
|
||||
None,
|
||||
),
|
||||
epic_ends_at_state: StyledDateTimeInputState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::EpicEndsAt),
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub enum Page {
|
||||
Project,
|
||||
@ -345,109 +90,6 @@ pub struct UpdateProjectForm {
|
||||
pub fields: UpdateProjectPayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProjectPage {
|
||||
pub text_filter: String,
|
||||
pub active_avatar_filters: Vec<UserId>,
|
||||
pub only_my_filter: bool,
|
||||
pub recently_updated_filter: bool,
|
||||
pub issue_drag: DragState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InvitePage {
|
||||
pub token: String,
|
||||
pub token_touched: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProjectSettingsPage {
|
||||
pub payload: UpdateProjectPayload,
|
||||
pub project_category_state: StyledSelectState,
|
||||
pub description_mode: crate::shared::styled_editor::Mode,
|
||||
pub time_tracking: StyledCheckboxState,
|
||||
pub column_drag: DragState,
|
||||
pub edit_column_id: Option<IssueStatusId>,
|
||||
pub creating_issue_status: bool,
|
||||
pub name: StyledInputState,
|
||||
// pub description_rte: StyledRteState,
|
||||
}
|
||||
|
||||
impl ProjectSettingsPage {
|
||||
pub fn new(project: &Project) -> Self {
|
||||
use crate::shared::styled_editor::Mode as EditorMode;
|
||||
let jirs_data::Project {
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
description,
|
||||
category,
|
||||
time_tracking,
|
||||
..
|
||||
} = project;
|
||||
Self {
|
||||
payload: UpdateProjectPayload {
|
||||
id: *id,
|
||||
name: Some(name.clone()),
|
||||
url: Some(url.clone()),
|
||||
description: Some(description.clone()),
|
||||
category: Some(*category),
|
||||
time_tracking: Some(*time_tracking),
|
||||
},
|
||||
description_mode: EditorMode::View,
|
||||
project_category_state: StyledSelectState::new(
|
||||
FieldId::ProjectSettings(ProjectFieldId::Category),
|
||||
vec![(*category).into()],
|
||||
),
|
||||
time_tracking: StyledCheckboxState::new(
|
||||
FieldId::ProjectSettings(ProjectFieldId::TimeTracking),
|
||||
(*time_tracking).into(),
|
||||
),
|
||||
column_drag: Default::default(),
|
||||
edit_column_id: None,
|
||||
creating_issue_status: false,
|
||||
name: StyledInputState::new(
|
||||
FieldId::ProjectSettings(ProjectFieldId::IssueStatusName),
|
||||
"",
|
||||
),
|
||||
// description_rte: StyledRteState::new(FieldId::ProjectSettings(
|
||||
// ProjectFieldId::Description,
|
||||
// )),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.edit_column_id = None;
|
||||
self.name.reset();
|
||||
self.creating_issue_status = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SignInPage {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub token: String,
|
||||
pub login_success: bool,
|
||||
pub bad_token: String,
|
||||
// touched
|
||||
pub username_touched: bool,
|
||||
pub email_touched: bool,
|
||||
pub token_touched: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SignUpPage {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub sign_up_success: bool,
|
||||
pub error: String,
|
||||
// touched
|
||||
pub username_touched: bool,
|
||||
pub email_touched: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialOrd, PartialEq)]
|
||||
pub enum InvitationFormState {
|
||||
Initial = 1,
|
||||
@ -462,96 +104,6 @@ impl Default for InvitationFormState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UsersPage {
|
||||
pub name: String,
|
||||
pub name_touched: bool,
|
||||
pub email: String,
|
||||
pub email_touched: bool,
|
||||
pub user_role: UserRole,
|
||||
|
||||
pub user_role_state: StyledSelectState,
|
||||
pub pending_invitations: Vec<String>,
|
||||
pub error: String,
|
||||
pub form_state: InvitationFormState,
|
||||
|
||||
pub invited_users: Vec<User>,
|
||||
pub invitations: Vec<Invitation>,
|
||||
}
|
||||
|
||||
impl Default for UsersPage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "".to_string(),
|
||||
name_touched: false,
|
||||
email: "".to_string(),
|
||||
email_touched: false,
|
||||
user_role: Default::default(),
|
||||
user_role_state: StyledSelectState::new(FieldId::Users(UsersFieldId::UserRole), vec![]),
|
||||
pending_invitations: vec![],
|
||||
error: "".to_string(),
|
||||
form_state: Default::default(),
|
||||
invited_users: vec![],
|
||||
invitations: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProfilePage {
|
||||
pub name: StyledInputState,
|
||||
pub email: StyledInputState,
|
||||
pub avatar: StyledImageInputState,
|
||||
pub current_project: StyledSelectState,
|
||||
}
|
||||
|
||||
impl ProfilePage {
|
||||
pub fn new(user: &User, project_ids: Vec<ProjectId>) -> Self {
|
||||
Self {
|
||||
name: StyledInputState::new(
|
||||
FieldId::Profile(UsersFieldId::Username),
|
||||
user.name.as_str(),
|
||||
),
|
||||
email: StyledInputState::new(
|
||||
FieldId::Profile(UsersFieldId::Email),
|
||||
user.email.as_str(),
|
||||
),
|
||||
avatar: StyledImageInputState::new(
|
||||
FieldId::Profile(UsersFieldId::Avatar),
|
||||
user.avatar_url.as_ref().cloned(),
|
||||
),
|
||||
current_project: StyledSelectState::new(
|
||||
FieldId::Profile(UsersFieldId::CurrentProject),
|
||||
project_ids.into_iter().map(|n| n as u32).collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReportsPage {
|
||||
pub selected_day: Option<chrono::NaiveDate>,
|
||||
pub hovered_day: Option<chrono::NaiveDate>,
|
||||
pub first_day: NaiveDate,
|
||||
pub last_day: NaiveDate,
|
||||
}
|
||||
|
||||
impl Default for ReportsPage {
|
||||
fn default() -> Self {
|
||||
let first_day = chrono::Utc::today().with_day(1).unwrap().naive_local();
|
||||
let last_day = (first_day + chrono::Duration::days(32))
|
||||
.with_day(1)
|
||||
.unwrap()
|
||||
- chrono::Duration::days(1);
|
||||
Self {
|
||||
first_day,
|
||||
last_day,
|
||||
selected_day: None,
|
||||
hovered_day: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PageContent {
|
||||
SignIn(Box<SignInPage>),
|
||||
@ -591,12 +143,22 @@ pub struct Model {
|
||||
pub page_content: PageContent,
|
||||
|
||||
pub project: Option<Project>,
|
||||
pub user: Option<User>,
|
||||
|
||||
pub current_user_project: Option<UserProject>,
|
||||
|
||||
pub issues: Vec<Issue>,
|
||||
pub issues_by_id: HashMap<IssueId, Issue>,
|
||||
|
||||
pub user: Option<User>,
|
||||
pub users: Vec<User>,
|
||||
pub users_by_id: HashMap<UserId, User>,
|
||||
|
||||
pub comments: Vec<Comment>,
|
||||
|
||||
pub issue_statuses: Vec<IssueStatus>,
|
||||
pub issue_statuses_by_id: HashMap<IssueStatusId, IssueStatus>,
|
||||
pub issue_statuses_by_name: HashMap<String, IssueStatus>,
|
||||
|
||||
pub messages: Vec<Message>,
|
||||
pub user_projects: Vec<UserProject>,
|
||||
pub projects: Vec<Project>,
|
||||
@ -605,8 +167,6 @@ pub struct Model {
|
||||
|
||||
impl Model {
|
||||
pub fn new(host_url: String, ws_url: String) -> Self {
|
||||
// let hi_worker = Worker::new("/hi.js");
|
||||
|
||||
Self {
|
||||
ws: None,
|
||||
ws_queue: vec![],
|
||||
@ -627,12 +187,16 @@ impl Model {
|
||||
messages_tooltip_visible: false,
|
||||
issues: vec![],
|
||||
users: vec![],
|
||||
users_by_id: Default::default(),
|
||||
comments: vec![],
|
||||
issue_statuses: vec![],
|
||||
issue_statuses_by_id: Default::default(),
|
||||
issue_statuses_by_name: Default::default(),
|
||||
messages: vec![],
|
||||
user_projects: vec![],
|
||||
projects: vec![],
|
||||
epics: vec![],
|
||||
issues_by_id: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -642,10 +206,4 @@ impl Model {
|
||||
.map(|up| up.role)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
// pub fn current_project_id(&self) -> ProjectId {
|
||||
// self.current_user_project
|
||||
// .as_ref()
|
||||
// .map(|up| up.project_id)
|
||||
// .unwrap_or_default()
|
||||
// }
|
||||
}
|
||||
|
7
jirs-client/src/pages/invite_page/mod.rs
Normal file
7
jirs-client/src/pages/invite_page/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub use model::*;
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
mod model;
|
||||
mod update;
|
||||
mod view;
|
6
jirs-client/src/pages/invite_page/model.rs
Normal file
6
jirs-client/src/pages/invite_page/model.rs
Normal file
@ -0,0 +1,6 @@
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InvitePage {
|
||||
pub token: String,
|
||||
pub token_touched: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
67
jirs-client/src/pages/invite_page/update.rs
Normal file
67
jirs-client/src/pages/invite_page/update.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::{fields::*, WsMsg};
|
||||
|
||||
use crate::{
|
||||
authorize_or_redirect,
|
||||
model::{Model, Page, PageContent},
|
||||
pages::invite_page::InvitePage,
|
||||
shared::write_auth_token,
|
||||
ws::send_ws_msg,
|
||||
FieldId, InvitationPageChange, Msg, PageChanged, WebSocketChanged,
|
||||
};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match model.page_content {
|
||||
PageContent::Invite(..) => (),
|
||||
_ if model.page == Page::Invite => build_page_content(model),
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::Invite(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(ws_msg)) => match ws_msg {
|
||||
WsMsg::InvitationAcceptFailure(_) => {
|
||||
page.error = Some("Invalid token".to_string());
|
||||
}
|
||||
WsMsg::InvitationAcceptSuccess(token) => {
|
||||
if let Ok(Msg::AuthTokenStored) = write_auth_token(Some(token)) {
|
||||
authorize_or_redirect(model, orders);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Msg::StrInputChanged(FieldId::Invite(InviteFieldId::Token), text) => {
|
||||
page.token_touched = true;
|
||||
page.token = text;
|
||||
page.error = None;
|
||||
}
|
||||
Msg::PageChanged(PageChanged::Invitation(InvitationPageChange::SubmitForm)) => {
|
||||
if let Ok(token) = uuid::Uuid::from_str(page.token.as_str()) {
|
||||
send_ws_msg(
|
||||
WsMsg::InvitationAcceptRequest(token),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
page.error = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_page_content(model: &mut Model) {
|
||||
let s: String = seed::document().location().unwrap().to_string().into();
|
||||
let url = seed::Url::from_str(s.as_str()).unwrap();
|
||||
let search = url.search();
|
||||
let values = search.get("token").cloned().unwrap_or_default();
|
||||
let mut content = InvitePage::default();
|
||||
content.token = values.get(0).cloned().unwrap_or_default();
|
||||
model.page_content = PageContent::Invite(Box::new(content));
|
||||
}
|
65
jirs-client/src/pages/invite_page/view.rs
Normal file
65
jirs-client/src/pages/invite_page/view.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use {
|
||||
crate::{
|
||||
model::{Model, PageContent},
|
||||
pages::invite_page::InvitePage,
|
||||
shared::{
|
||||
outer_layout, styled_button::StyledButton, styled_field::StyledField,
|
||||
styled_form::StyledForm, styled_input::StyledInput, ToNode,
|
||||
},
|
||||
validations::is_token,
|
||||
FieldId, InvitationPageChange, Msg, PageChanged,
|
||||
},
|
||||
jirs_data::fields::*,
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
PageContent::Invite(page) => page,
|
||||
_ => return empty![],
|
||||
};
|
||||
|
||||
let token_field = token_field(page);
|
||||
let submit_field = submit(page);
|
||||
let error = match page.error.as_ref() {
|
||||
Some(s) => div![C!["error"], s.as_str()],
|
||||
_ => empty![],
|
||||
};
|
||||
|
||||
let form = StyledForm::build()
|
||||
.heading("Welcome in JIRS")
|
||||
.on_submit(ev(Ev::Submit, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::PageChanged(PageChanged::Invitation(InvitationPageChange::SubmitForm))
|
||||
}))
|
||||
.add_field(token_field)
|
||||
.add_field(submit_field)
|
||||
.add_field(error)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
outer_layout(model, "invite", vec![form])
|
||||
}
|
||||
|
||||
fn submit(_page: &InvitePage) -> Node<Msg> {
|
||||
let submit = StyledButton::build()
|
||||
.text("Accept")
|
||||
.primary()
|
||||
.build()
|
||||
.into_node();
|
||||
StyledField::build().input(submit).build().into_node()
|
||||
}
|
||||
|
||||
fn token_field(page: &InvitePage) -> Node<Msg> {
|
||||
let token = StyledInput::build()
|
||||
.valid(!page.token_touched || is_token(page.token.as_str()) && page.error.is_none())
|
||||
.value(page.token.as_str())
|
||||
.build(FieldId::Invite(InviteFieldId::Token))
|
||||
.into_node();
|
||||
|
||||
StyledField::build()
|
||||
.input(token)
|
||||
.label("Your invite token")
|
||||
.build()
|
||||
.into_node()
|
||||
}
|
8
jirs-client/src/pages/mod.rs
Normal file
8
jirs-client/src/pages/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub mod invite_page;
|
||||
pub mod profile_page;
|
||||
pub mod project_page;
|
||||
pub mod project_settings_page;
|
||||
pub mod reports_page;
|
||||
pub mod sign_in_page;
|
||||
pub mod sign_up_page;
|
||||
pub mod users_page;
|
6
jirs-client/src/pages/profile_page/mod.rs
Normal file
6
jirs-client/src/pages/profile_page/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
pub mod model;
|
||||
pub mod update;
|
||||
pub mod view;
|
40
jirs-client/src/pages/profile_page/model.rs
Normal file
40
jirs-client/src/pages/profile_page/model.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use jirs_data::{ProjectId, User, UsersFieldId};
|
||||
|
||||
use crate::{
|
||||
shared::{
|
||||
styled_image_input::StyledImageInputState, styled_input::StyledInputState,
|
||||
styled_select::StyledSelectState,
|
||||
},
|
||||
FieldId,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProfilePage {
|
||||
pub name: StyledInputState,
|
||||
pub email: StyledInputState,
|
||||
pub avatar: StyledImageInputState,
|
||||
pub current_project: StyledSelectState,
|
||||
}
|
||||
|
||||
impl ProfilePage {
|
||||
pub fn new(user: &User, project_ids: Vec<ProjectId>) -> Self {
|
||||
Self {
|
||||
name: StyledInputState::new(
|
||||
FieldId::Profile(UsersFieldId::Username),
|
||||
user.name.as_str(),
|
||||
),
|
||||
email: StyledInputState::new(
|
||||
FieldId::Profile(UsersFieldId::Email),
|
||||
user.email.as_str(),
|
||||
),
|
||||
avatar: StyledImageInputState::new(
|
||||
FieldId::Profile(UsersFieldId::Avatar),
|
||||
user.avatar_url.as_ref().cloned(),
|
||||
),
|
||||
current_project: StyledSelectState::new(
|
||||
FieldId::Profile(UsersFieldId::CurrentProject),
|
||||
project_ids.into_iter().map(|n| n as u32).collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
use seed::prelude::{Method, Orders, Request};
|
||||
use web_sys::FormData;
|
||||
|
||||
use jirs_data::{ProjectId, UsersFieldId, WsMsg};
|
||||
|
||||
use crate::model::{Model, Page, PageContent, ProfilePage};
|
||||
use crate::shared::styled_select::StyledSelectChanged;
|
||||
use crate::ws::{board_load, send_ws_msg};
|
||||
use crate::{FieldId, Msg, PageChanged, ProfilePageChange, WebSocketChanged};
|
||||
use {
|
||||
crate::{
|
||||
model::{Model, Page, PageContent},
|
||||
pages::profile_page::model::ProfilePage,
|
||||
shared::styled_select::StyledSelectChanged,
|
||||
ws::{board_load, send_ws_msg},
|
||||
FieldId, Msg, OperationKind, PageChanged, ProfilePageChange, ResourceKind,
|
||||
WebSocketChanged,
|
||||
},
|
||||
jirs_data::{ProjectId, User, UsersFieldId, WsMsg},
|
||||
seed::prelude::{Method, Orders, Request},
|
||||
web_sys::FormData,
|
||||
};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
|
||||
Msg::ResourceChanged(ResourceKind::Auth, OperationKind::SingleLoaded, Some(_))
|
||||
| Msg::ChangePage(Page::Profile) => {
|
||||
board_load(model, orders);
|
||||
build_page_content(model);
|
||||
@ -45,12 +49,13 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
||||
orders.perform_cmd(update_avatar(fd, model.host_url.clone()));
|
||||
orders.skip();
|
||||
}
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(ws_msg)) => {
|
||||
if let WsMsg::AvatarUrlChanged(user_id, avatar_url) = ws_msg {
|
||||
if let Some(me) = model.user.as_mut() {
|
||||
if me.id == user_id {
|
||||
profile_page.avatar.url = Some(avatar_url);
|
||||
}
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AvatarUrlChanged(
|
||||
user_id,
|
||||
avatar_url,
|
||||
))) => {
|
||||
if let Some(User { id, .. }) = model.user.as_mut() {
|
||||
if *id == user_id {
|
||||
profile_page.avatar.url = Some(avatar_url);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::model::{Model, PageContent, ProfilePage};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_image_input::StyledImageInput;
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_select::StyledSelect;
|
||||
use crate::shared::{inner_layout, ToChild, ToNode};
|
||||
use crate::{FieldId, Msg, PageChanged, ProfilePageChange};
|
||||
use {
|
||||
crate::{
|
||||
model::{Model, PageContent},
|
||||
pages::profile_page::model::ProfilePage,
|
||||
shared::{
|
||||
inner_layout, styled_button::StyledButton, styled_field::StyledField,
|
||||
styled_form::StyledForm, styled_image_input::StyledImageInput,
|
||||
styled_input::StyledInput, styled_select::StyledSelect, ToChild, ToNode,
|
||||
},
|
||||
FieldId, Msg, PageChanged, ProfilePageChange,
|
||||
},
|
||||
jirs_data::*,
|
||||
seed::{prelude::*, *},
|
||||
std::collections::HashMap,
|
||||
};
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
@ -122,7 +122,7 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
|
||||
};
|
||||
StyledField::build()
|
||||
.label("Current project")
|
||||
.input(div![class!["project-name"], inner])
|
||||
.input(div![C!["project-name"], inner])
|
||||
.build()
|
||||
.into_node()
|
||||
}
|
6
jirs-client/src/pages/project_page/mod.rs
Normal file
6
jirs-client/src/pages/project_page/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
pub mod model;
|
||||
pub mod update;
|
||||
pub mod view;
|
118
jirs-client/src/pages/project_page/model.rs
Normal file
118
jirs-client/src/pages/project_page/model.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::shared::drag::DragState;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct StatusIssueIds {
|
||||
pub status_id: IssueStatusId,
|
||||
pub status_name: IssueStatusName,
|
||||
pub issue_ids: Vec<IssueId>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct EpicIssuePerStatus {
|
||||
pub epic_name: EpicName,
|
||||
pub per_status_issues: Vec<StatusIssueIds>,
|
||||
}
|
||||
|
||||
// pub type VisibleIssueMap =
|
||||
// HashMap<EpicName, HashMap<(IssueStatusId, IssueStatusName), Vec<IssueId>>>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProjectPage {
|
||||
pub text_filter: String,
|
||||
pub active_avatar_filters: Vec<UserId>,
|
||||
pub only_my_filter: bool,
|
||||
pub recently_updated_filter: bool,
|
||||
pub issue_drag: DragState,
|
||||
pub visible_issues: Vec<EpicIssuePerStatus>,
|
||||
}
|
||||
|
||||
impl ProjectPage {
|
||||
pub fn rebuild_visible(
|
||||
&mut self,
|
||||
epics: &Vec<Epic>,
|
||||
statuses: &Vec<IssueStatus>,
|
||||
issues: &Vec<Issue>,
|
||||
user: &Option<User>,
|
||||
) {
|
||||
let mut map = vec![];
|
||||
let epics = vec![None]
|
||||
.into_iter()
|
||||
.chain(epics.iter().map(|s| Some((s.id, s.name.as_str()))));
|
||||
|
||||
let statuses = statuses.iter().map(|s| (s.id, s.name.as_str()));
|
||||
|
||||
let mut issues: Vec<&Issue> = {
|
||||
let mut m = HashMap::new();
|
||||
let mut sorted = vec![];
|
||||
for issue in issues.iter() {
|
||||
sorted.push((issue.id, issue.updated_at));
|
||||
m.insert(issue.id, issue);
|
||||
}
|
||||
sorted.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time));
|
||||
sorted
|
||||
.into_iter()
|
||||
.flat_map(|(id, _)| m.remove(&id))
|
||||
.collect()
|
||||
};
|
||||
if self.recently_updated_filter {
|
||||
issues = issues[0..10].to_vec()
|
||||
}
|
||||
|
||||
for epic in epics {
|
||||
let mut per_epic_map = EpicIssuePerStatus::default();
|
||||
per_epic_map.epic_name = epic.map(|(_, name)| name).unwrap_or_default().to_string();
|
||||
|
||||
for (current_status_id, issue_status_name) in statuses.clone() {
|
||||
let mut per_status_map = StatusIssueIds::default();
|
||||
per_status_map.status_id = current_status_id;
|
||||
per_status_map.status_name = issue_status_name.to_string();
|
||||
for issue in issues.iter() {
|
||||
if issue.epic_id == epic.map(|(id, _)| id)
|
||||
&& issue_filter_status(issue, current_status_id)
|
||||
&& issue_filter_with_avatars(issue, &self.active_avatar_filters)
|
||||
&& issue_filter_with_text(issue, self.text_filter.as_str())
|
||||
&& issue_filter_with_only_my(issue, self.only_my_filter, user)
|
||||
{
|
||||
per_status_map.issue_ids.push(issue.id);
|
||||
}
|
||||
}
|
||||
per_epic_map.per_status_issues.push(per_status_map);
|
||||
}
|
||||
map.push(per_epic_map);
|
||||
}
|
||||
self.visible_issues = map;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_avatars(issue: &Issue, user_ids: &[UserId]) -> bool {
|
||||
if user_ids.is_empty() {
|
||||
return true;
|
||||
}
|
||||
user_ids.contains(&issue.reporter_id) || issue.user_ids.iter().any(|id| user_ids.contains(id))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_status(issue: &Issue, current_status_id: IssueStatusId) -> bool {
|
||||
issue.issue_status_id == current_status_id
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_text(issue: &Issue, text: &str) -> bool {
|
||||
text.is_empty() || issue.title.contains(text)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_only_my(issue: &Issue, only_my: bool, user: &Option<User>) -> bool {
|
||||
let my_id = user.as_ref().map(|u| u.id).unwrap_or_default();
|
||||
!only_my || issue.user_ids.contains(&my_id)
|
||||
}
|
||||
|
||||
// #[inline]
|
||||
// fn issue_filter_with_only_recent(issue: &Issue, ids: &[IssueId]) -> bool {
|
||||
// ids.is_empty() || ids.contains(&issue.id)
|
||||
// }
|
@ -1,11 +1,16 @@
|
||||
use seed::prelude::Orders;
|
||||
use {
|
||||
crate::{
|
||||
model::{ModalType, Model, Page, PageContent},
|
||||
pages::project_page::model::ProjectPage,
|
||||
shared::styled_select::StyledSelectChanged,
|
||||
ws::{board_load, send_ws_msg},
|
||||
BoardPageChange, EditIssueModalSection, FieldId, Msg, PageChanged, WebSocketChanged,
|
||||
},
|
||||
jirs_data::*,
|
||||
seed::prelude::Orders,
|
||||
};
|
||||
|
||||
use jirs_data::{Issue, IssueFieldId, WsMsg};
|
||||
|
||||
use crate::model::{ModalType, Model, Page, PageContent, ProjectPage};
|
||||
use crate::shared::styled_select::StyledSelectChanged;
|
||||
use crate::ws::{board_load, send_ws_msg};
|
||||
use crate::{BoardPageChange, EditIssueModalSection, FieldId, Msg, PageChanged, WebSocketChanged};
|
||||
use crate::{OperationKind, ResourceKind};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.user.is_none() {
|
||||
@ -28,34 +33,35 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
||||
|
||||
match msg {
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
|
||||
| Msg::UserChanged(..)
|
||||
| Msg::ProjectChanged(Some(..))
|
||||
| Msg::ChangePage(Page::Project)
|
||||
| Msg::ChangePage(Page::AddIssue)
|
||||
| Msg::ChangePage(Page::EditIssue(..)) => {
|
||||
board_load(model, orders);
|
||||
}
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
|
||||
let mut old: Vec<Issue> = vec![];
|
||||
std::mem::swap(&mut old, &mut model.issues);
|
||||
for is in old {
|
||||
if is.id == issue.id {
|
||||
model.issues.push(issue.clone())
|
||||
} else {
|
||||
model.issues.push(is);
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueDeleted(id, count)))
|
||||
if count > 0 =>
|
||||
{
|
||||
let mut old: Vec<Issue> = vec![];
|
||||
std::mem::swap(&mut old, &mut model.issues);
|
||||
for is in old {
|
||||
if is.id != id {
|
||||
model.issues.push(is);
|
||||
}
|
||||
}
|
||||
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleRemoved, ..) => {
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
project_page.rebuild_visible(
|
||||
&model.epics,
|
||||
&model.issue_statuses,
|
||||
&model.issues,
|
||||
&model.user,
|
||||
);
|
||||
}
|
||||
Msg::ResourceChanged(
|
||||
ResourceKind::Issue
|
||||
| ResourceKind::Project
|
||||
| ResourceKind::IssueStatus
|
||||
| ResourceKind::Epic,
|
||||
..,
|
||||
) => {
|
||||
project_page.rebuild_visible(
|
||||
&model.epics,
|
||||
&model.issue_statuses,
|
||||
&model.issues,
|
||||
&model.user,
|
||||
);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
@ -1,15 +1,19 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::model::{Model, PageContent};
|
||||
use crate::shared::styled_avatar::StyledAvatar;
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::{inner_layout, ToNode};
|
||||
use crate::{BoardPageChange, FieldId, Msg, PageChanged};
|
||||
use {
|
||||
crate::{
|
||||
model::{Model, Page, PageContent},
|
||||
shared::{
|
||||
inner_layout,
|
||||
styled_avatar::StyledAvatar,
|
||||
styled_button::StyledButton,
|
||||
styled_icon::{Icon, StyledIcon},
|
||||
styled_input::StyledInput,
|
||||
ToNode,
|
||||
},
|
||||
BoardPageChange, FieldId, Msg, PageChanged,
|
||||
},
|
||||
jirs_data::*,
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let project_section = vec![
|
||||
@ -29,11 +33,11 @@ fn breadcrumbs(model: &Model) -> Node<Msg> {
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_default();
|
||||
div![
|
||||
class!["breadcrumbsContainer"],
|
||||
C!["breadcrumbsContainer"],
|
||||
span!["Projects"],
|
||||
span![class!["breadcrumbsDivider"], "/"],
|
||||
span![C!["breadcrumbsDivider"], "/"],
|
||||
span![project_name],
|
||||
span![class!["breadcrumbsDivider"], "/"],
|
||||
span![C!["breadcrumbsDivider"], "/"],
|
||||
span!["Kanban Board"]
|
||||
]
|
||||
}
|
||||
@ -47,7 +51,7 @@ fn header() -> Node<Msg> {
|
||||
.into_node();
|
||||
div![
|
||||
id!["projectBoardHeader"],
|
||||
div![id!["boardName"], class!["headerChild"], "Kanban board"],
|
||||
div![id!["boardName"], C!["headerChild"], "Kanban board"],
|
||||
a![
|
||||
attrs![At::Href => "https://gitlab.com/adrian.wozniak/jirs", At::Target => "__blank", At::Rel => "noreferrer noopener"],
|
||||
button
|
||||
@ -91,7 +95,7 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
{
|
||||
seed::button![
|
||||
id!["clearAllFilters"],
|
||||
class!["filterChild"],
|
||||
C!["filterChild"],
|
||||
"Clear all",
|
||||
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
|
||||
]
|
||||
@ -139,94 +143,77 @@ fn avatars_filters(model: &Model) -> Node<Msg> {
|
||||
})
|
||||
.collect();
|
||||
|
||||
div![id!["avatars"], class!["filterChild"], avatars]
|
||||
div![id!["avatars"], C!["filterChild"], avatars]
|
||||
}
|
||||
|
||||
fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||
let mut rows: Vec<Option<&Epic>> = vec![None];
|
||||
for epic in model.epics.iter() {
|
||||
rows.push(Some(epic));
|
||||
}
|
||||
let rows: Vec<Node<Msg>> = rows
|
||||
.into_iter()
|
||||
.map(|epic| {
|
||||
let title = epic
|
||||
.map(|epic| div![C!["rowName"], epic.name.as_str()])
|
||||
.unwrap_or_else(|| empty![]);
|
||||
let columns: Vec<Node<Msg>> = model
|
||||
.issue_statuses
|
||||
.iter()
|
||||
.map(|issue_status| project_issue_list(model, issue_status, epic))
|
||||
.collect();
|
||||
div![C!["row"], title, div![C!["projectBoardLists"], columns]]
|
||||
})
|
||||
.collect();
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let rows = project_page.visible_issues.iter().map(|per_epic| {
|
||||
let columns: Vec<Node<Msg>> = per_epic
|
||||
.per_status_issues
|
||||
.iter()
|
||||
.map(|per_status| {
|
||||
let issues: Vec<&Issue> = per_status
|
||||
.issue_ids
|
||||
.iter()
|
||||
.filter_map(|id| model.issues_by_id.get(id))
|
||||
.collect();
|
||||
project_issue_list(
|
||||
model,
|
||||
per_status.status_id,
|
||||
&per_status.status_name,
|
||||
issues.as_slice(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
div![
|
||||
C!["row"],
|
||||
div![C!["epicName"], per_epic.epic_name.as_str()],
|
||||
div![C!["projectBoardLists"], columns]
|
||||
]
|
||||
});
|
||||
div![C!["rows"], rows]
|
||||
}
|
||||
|
||||
fn project_issue_list(
|
||||
model: &Model,
|
||||
status: &jirs_data::IssueStatus,
|
||||
epic: Option<&jirs_data::Epic>,
|
||||
status_id: IssueStatusId,
|
||||
status_name: &str,
|
||||
issues: &[&Issue],
|
||||
) -> Node<Msg> {
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let ids: Vec<IssueId> = if project_page.recently_updated_filter {
|
||||
let mut v: Vec<(IssueId, NaiveDateTime)> = model
|
||||
.issues
|
||||
.iter()
|
||||
.map(|issue| (issue.id, issue.updated_at))
|
||||
.collect();
|
||||
v.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time));
|
||||
if v.len() > 10 { v[0..10].to_vec() } else { v }
|
||||
.into_iter()
|
||||
.map(|(id, _)| id)
|
||||
.collect()
|
||||
} else {
|
||||
model.issues.iter().map(|issue| issue.id).collect()
|
||||
};
|
||||
let issues: Vec<Node<Msg>> = model
|
||||
.issues
|
||||
let issues: Vec<Node<Msg>> = issues
|
||||
.iter()
|
||||
.filter(|issue| {
|
||||
issue.epic_id == epic.map(|epic| epic.id)
|
||||
&& issue_filter_status(issue, status)
|
||||
&& issue_filter_with_avatars(issue, &project_page.active_avatar_filters)
|
||||
&& issue_filter_with_text(issue, project_page.text_filter.as_str())
|
||||
&& issue_filter_with_only_my(issue, project_page.only_my_filter, &model.user)
|
||||
&& issue_filter_with_only_recent(issue, ids.as_slice())
|
||||
})
|
||||
.map(|issue| project_issue(model, issue))
|
||||
.collect();
|
||||
let label = status.name.clone();
|
||||
let drop_handler = {
|
||||
let send_status = status_id;
|
||||
drag_ev(Ev::Drop, move |ev| {
|
||||
ev.prevent_default();
|
||||
Some(Msg::PageChanged(PageChanged::Board(
|
||||
BoardPageChange::IssueDropZone(send_status),
|
||||
)))
|
||||
})
|
||||
};
|
||||
|
||||
let send_status = status.id;
|
||||
let drop_handler = drag_ev(Ev::Drop, move |ev| {
|
||||
ev.prevent_default();
|
||||
Some(Msg::PageChanged(PageChanged::Board(
|
||||
BoardPageChange::IssueDropZone(send_status),
|
||||
)))
|
||||
});
|
||||
|
||||
let send_status = status.id;
|
||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
Some(Msg::PageChanged(PageChanged::Board(
|
||||
BoardPageChange::IssueDragOverStatus(send_status),
|
||||
)))
|
||||
});
|
||||
let drag_over_handler = {
|
||||
let send_status = status_id;
|
||||
drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
Some(Msg::PageChanged(PageChanged::Board(
|
||||
BoardPageChange::IssueDragOverStatus(send_status),
|
||||
)))
|
||||
})
|
||||
};
|
||||
|
||||
div![
|
||||
attrs![At::Class => "list";],
|
||||
C!["list"],
|
||||
div![C!["title"], status_name, div![C!["issuesCount"]]],
|
||||
div![
|
||||
attrs![At::Class => "title"],
|
||||
label,
|
||||
div![attrs![At::Class => "issuesCount"]]
|
||||
],
|
||||
div![
|
||||
attrs![At::Class => "issues"; At::DropZone => "link"],
|
||||
C!["issues"],
|
||||
attrs![At::DropZone => "link"],
|
||||
drop_handler,
|
||||
drag_over_handler,
|
||||
issues
|
||||
@ -234,58 +221,41 @@ fn project_issue_list(
|
||||
]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_avatars(issue: &Issue, user_ids: &[UserId]) -> bool {
|
||||
if user_ids.is_empty() {
|
||||
return true;
|
||||
}
|
||||
user_ids.contains(&issue.reporter_id) || issue.user_ids.iter().any(|id| user_ids.contains(id))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_status(issue: &Issue, status: &IssueStatus) -> bool {
|
||||
issue.issue_status_id == status.id
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_text(issue: &Issue, text: &str) -> bool {
|
||||
text.is_empty() || issue.title.contains(text)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_only_my(issue: &Issue, only_my: bool, user: &Option<User>) -> bool {
|
||||
let my_id = user.as_ref().map(|u| u.id).unwrap_or_default();
|
||||
!only_my || issue.user_ids.contains(&my_id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_only_recent(issue: &Issue, ids: &[IssueId]) -> bool {
|
||||
ids.is_empty() || ids.contains(&issue.id)
|
||||
}
|
||||
|
||||
fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||
let avatars: Vec<Node<Msg>> = model
|
||||
.users
|
||||
let avatars: Vec<Node<Msg>> = issue
|
||||
.user_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, user)| issue.user_ids.contains(&user.id))
|
||||
.map(|(idx, user)| {
|
||||
.filter_map(|id| model.users_by_id.get(id))
|
||||
.map(|user| {
|
||||
StyledAvatar::build()
|
||||
.size(24)
|
||||
.name(user.name.as_str())
|
||||
.avatar_url(user.avatar_url.as_deref().unwrap_or_default())
|
||||
.user_index(idx)
|
||||
.user_index(0)
|
||||
.build()
|
||||
.into_node()
|
||||
})
|
||||
.collect();
|
||||
// let avatars: Vec<Node<Msg>> = model
|
||||
// .users
|
||||
// .iter()
|
||||
// .enumerate()
|
||||
// .filter(|(_, user)| issue.user_ids.contains(&user.id))
|
||||
// .map(|(idx, user)| {
|
||||
// StyledAvatar::build()
|
||||
// .size(24)
|
||||
// .name(user.name.as_str())
|
||||
// .avatar_url(user.avatar_url.as_deref().unwrap_or_default())
|
||||
// .user_index(idx)
|
||||
// .build()
|
||||
// .into_node()
|
||||
// })
|
||||
// .collect();
|
||||
|
||||
let issue_type_icon = {
|
||||
StyledIcon::build(issue.issue_type.clone().into())
|
||||
.with_color(issue.issue_type.to_str())
|
||||
.build()
|
||||
.into_node()
|
||||
};
|
||||
let issue_type_icon = StyledIcon::build(issue.issue_type.clone().into())
|
||||
.with_color(issue.issue_type.to_str())
|
||||
.build()
|
||||
.into_node();
|
||||
let priority_icon = {
|
||||
let icon = match issue.priority {
|
||||
IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown,
|
||||
@ -315,33 +285,43 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||
BoardPageChange::ExchangePosition(issue_id),
|
||||
)))
|
||||
});
|
||||
let issue_id = issue.id;
|
||||
|
||||
let drag_out = drag_ev(Ev::DragLeave, move |_| {
|
||||
Some(Msg::PageChanged(PageChanged::Board(
|
||||
BoardPageChange::DragLeave(issue_id),
|
||||
)))
|
||||
});
|
||||
|
||||
let class_list = vec!["issue"];
|
||||
let on_click = mouse_ev("click", move |ev| {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
seed::Url::new()
|
||||
.add_path_part("issues")
|
||||
.add_path_part(format!("{}", issue_id))
|
||||
.go_and_push();
|
||||
Msg::ChangePage(Page::EditIssue(issue_id))
|
||||
});
|
||||
|
||||
let href = format!("/issues/{id}", id = issue_id);
|
||||
|
||||
a![
|
||||
drag_started,
|
||||
attrs![At::Class => "issueLink"; At::Href => href],
|
||||
on_click,
|
||||
C!["issueLink"],
|
||||
attrs![At::Href => href],
|
||||
div![
|
||||
attrs![At::Class => class_list.join(" "), At::Draggable => true],
|
||||
C!["issue"],
|
||||
attrs![At::Draggable => true],
|
||||
drag_stopped,
|
||||
drag_over_handler,
|
||||
drag_out,
|
||||
p![attrs![At::Class => "title"], issue.title.as_str()],
|
||||
p![C!["title"], issue.title.as_str()],
|
||||
div![
|
||||
attrs![At::Class => "bottom"],
|
||||
C!["bottom"],
|
||||
div![
|
||||
div![attrs![At::Class => "issueTypeIcon"], issue_type_icon],
|
||||
div![attrs![At::Class => "issuePriorityIcon"], priority_icon]
|
||||
div![C!["issueTypeIcon"], issue_type_icon],
|
||||
div![C!["issuePriorityIcon"], priority_icon]
|
||||
],
|
||||
div![attrs![At::Class => "assignees"], avatars,],
|
||||
div![C!["assignees"], avatars,],
|
||||
]
|
||||
]
|
||||
]
|
7
jirs-client/src/pages/project_settings_page/mod.rs
Normal file
7
jirs-client/src/pages/project_settings_page/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub use model::*;
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
mod model;
|
||||
mod update;
|
||||
mod view;
|
72
jirs-client/src/pages/project_settings_page/model.rs
Normal file
72
jirs-client/src/pages/project_settings_page/model.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use jirs_data::{IssueStatusId, Project, ProjectFieldId, UpdateProjectPayload};
|
||||
|
||||
use crate::{
|
||||
shared::{
|
||||
drag::DragState, styled_checkbox::StyledCheckboxState, styled_input::StyledInputState,
|
||||
styled_select::StyledSelectState,
|
||||
},
|
||||
FieldId,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProjectSettingsPage {
|
||||
pub payload: UpdateProjectPayload,
|
||||
pub project_category_state: StyledSelectState,
|
||||
pub description_mode: crate::shared::styled_editor::Mode,
|
||||
pub time_tracking: StyledCheckboxState,
|
||||
pub column_drag: DragState,
|
||||
pub edit_column_id: Option<IssueStatusId>,
|
||||
pub creating_issue_status: bool,
|
||||
pub name: StyledInputState,
|
||||
// pub description_rte: StyledRteState,
|
||||
}
|
||||
|
||||
impl ProjectSettingsPage {
|
||||
pub fn new(project: &Project) -> Self {
|
||||
use crate::shared::styled_editor::Mode as EditorMode;
|
||||
let jirs_data::Project {
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
description,
|
||||
category,
|
||||
time_tracking,
|
||||
..
|
||||
} = project;
|
||||
Self {
|
||||
payload: UpdateProjectPayload {
|
||||
id: *id,
|
||||
name: Some(name.clone()),
|
||||
url: Some(url.clone()),
|
||||
description: Some(description.clone()),
|
||||
category: Some(*category),
|
||||
time_tracking: Some(*time_tracking),
|
||||
},
|
||||
description_mode: EditorMode::View,
|
||||
project_category_state: StyledSelectState::new(
|
||||
FieldId::ProjectSettings(ProjectFieldId::Category),
|
||||
vec![(*category).into()],
|
||||
),
|
||||
time_tracking: StyledCheckboxState::new(
|
||||
FieldId::ProjectSettings(ProjectFieldId::TimeTracking),
|
||||
(*time_tracking).into(),
|
||||
),
|
||||
column_drag: Default::default(),
|
||||
edit_column_id: None,
|
||||
creating_issue_status: false,
|
||||
name: StyledInputState::new(
|
||||
FieldId::ProjectSettings(ProjectFieldId::IssueStatusName),
|
||||
"",
|
||||
),
|
||||
// description_rte: StyledRteState::new(FieldId::ProjectSettings(
|
||||
// ProjectFieldId::Description,
|
||||
// )),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.edit_column_id = None;
|
||||
self.name.reset();
|
||||
self.creating_issue_status = false;
|
||||
}
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
use std::collections::HashSet;
|
||||
use {
|
||||
crate::{
|
||||
model::{Model, Page, PageContent},
|
||||
shared::styled_select::StyledSelectChanged,
|
||||
ws::{board_load, send_ws_msg},
|
||||
FieldChange::TabChanged,
|
||||
FieldId, Msg, PageChanged, ProjectPageChange, WebSocketChanged,
|
||||
},
|
||||
jirs_data::{IssueStatus, IssueStatusId, ProjectFieldId, UpdateProjectPayload, WsMsg},
|
||||
seed::{error, prelude::Orders},
|
||||
std::collections::HashSet,
|
||||
};
|
||||
|
||||
use seed::error;
|
||||
use seed::prelude::Orders;
|
||||
|
||||
use jirs_data::{IssueStatus, IssueStatusId, ProjectFieldId, UpdateProjectPayload, WsMsg};
|
||||
|
||||
use crate::model::{Model, Page, PageContent, ProjectSettingsPage};
|
||||
use crate::shared::styled_select::StyledSelectChanged;
|
||||
use crate::ws::{board_load, send_ws_msg};
|
||||
use crate::FieldChange::TabChanged;
|
||||
use crate::{FieldId, Msg, PageChanged, ProjectPageChange, WebSocketChanged};
|
||||
use crate::pages::project_settings_page::ProjectSettingsPage;
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.page != Page::ProjectSettings {
|
@ -4,19 +4,27 @@ use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{IssueStatus, ProjectCategory, TimeTracking, ToVec};
|
||||
|
||||
use crate::model::{DeleteIssueStatusModal, ModalType, Model, PageContent, ProjectSettingsPage};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_checkbox::StyledCheckbox;
|
||||
use crate::shared::styled_editor::StyledEditor;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::{inner_layout, ToChild, ToNode};
|
||||
use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange};
|
||||
use crate::{
|
||||
modals::issue_statuses_delete::Model as DeleteIssueStatusModal,
|
||||
model::{self, ModalType, Model, PageContent},
|
||||
pages::project_settings_page::ProjectSettingsPage,
|
||||
shared::{
|
||||
inner_layout,
|
||||
styled_button::StyledButton,
|
||||
styled_checkbox::StyledCheckbox,
|
||||
styled_editor::StyledEditor,
|
||||
styled_field::StyledField,
|
||||
styled_form::StyledForm,
|
||||
styled_icon::{Icon, StyledIcon},
|
||||
styled_input::StyledInput,
|
||||
styled_select::StyledSelect,
|
||||
styled_textarea::StyledTextarea,
|
||||
ToChild, ToNode,
|
||||
},
|
||||
FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange,
|
||||
};
|
||||
|
||||
// use crate::shared::styled_rte::StyledRte;
|
||||
use crate::shared::styled_select::StyledSelect;
|
||||
use crate::shared::styled_textarea::StyledTextarea;
|
||||
|
||||
static TIME_TRACKING_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt");
|
||||
static TIME_TRACKING_HOURLY: &str = include_str!("./time_tracking_hourly.txt");
|
||||
@ -98,7 +106,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let project_section = vec![div![class!["formContainer"], form]];
|
||||
let project_section = vec![div![C!["formContainer"], form]];
|
||||
|
||||
inner_layout(model, "projectSettings", project_section)
|
||||
}
|
||||
@ -201,9 +209,9 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
|
||||
.collect();
|
||||
|
||||
let columns_section = section![
|
||||
class!["columnsSection"],
|
||||
C!["columnsSection"],
|
||||
div![
|
||||
class!["columns"],
|
||||
C!["columns"],
|
||||
columns,
|
||||
add_column(page, column_style.as_str())
|
||||
]
|
||||
@ -246,15 +254,15 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
class!["columnPreview"],
|
||||
div![class!["columnName"], form![on_submit, input]]
|
||||
C!["columnPreview"],
|
||||
div![C!["columnName"], form![on_submit, input]]
|
||||
]
|
||||
} else {
|
||||
let add_column = StyledIcon::build(Icon::Plus).build().into_node();
|
||||
div![
|
||||
class!["columnPreview"],
|
||||
C!["columnPreview"],
|
||||
attrs![At::Style => column_style],
|
||||
div![class!["columnName addColumn"], add_column],
|
||||
div![C!["columnName addColumn"], add_column],
|
||||
on_click,
|
||||
]
|
||||
}
|
||||
@ -280,7 +288,7 @@ fn column_preview(
|
||||
.build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
|
||||
.into_node();
|
||||
|
||||
div![class!["columnPreview"], div![class!["columnName"], input]]
|
||||
div![C!["columnPreview"], div![C!["columnName"], input]]
|
||||
} else {
|
||||
show_column_preview(is, per_column_issue_count, column_style)
|
||||
}
|
||||
@ -336,19 +344,19 @@ fn show_column_preview(
|
||||
.on_click(on_delete)
|
||||
.build()
|
||||
.into_node();
|
||||
div![class!["removeColumn"], delete]
|
||||
div![C!["removeColumn"], delete]
|
||||
} else {
|
||||
div![
|
||||
class!["issueCount"],
|
||||
C!["issueCount"],
|
||||
format!("Issues in column: {}", issue_count_in_column)
|
||||
]
|
||||
};
|
||||
|
||||
div![
|
||||
class!["columnPreview"],
|
||||
C!["columnPreview"],
|
||||
attrs![At::Style => column_style, At::Draggable => "true", At::DropZone => "true"],
|
||||
div![
|
||||
class!["columnName"],
|
||||
C!["columnName"],
|
||||
span![is.name.as_str()],
|
||||
on_edit,
|
||||
delete_row
|
6
jirs-client/src/pages/reports_page/mod.rs
Normal file
6
jirs-client/src/pages/reports_page/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
pub mod model;
|
||||
pub mod update;
|
||||
pub mod view;
|
25
jirs-client/src/pages/reports_page/model.rs
Normal file
25
jirs-client/src/pages/reports_page/model.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use chrono::{prelude::*, NaiveDate};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReportsPage {
|
||||
pub selected_day: Option<chrono::NaiveDate>,
|
||||
pub hovered_day: Option<chrono::NaiveDate>,
|
||||
pub first_day: NaiveDate,
|
||||
pub last_day: NaiveDate,
|
||||
}
|
||||
|
||||
impl Default for ReportsPage {
|
||||
fn default() -> Self {
|
||||
let first_day = chrono::Utc::today().with_day(1).unwrap().naive_local();
|
||||
let last_day = (first_day + chrono::Duration::days(32))
|
||||
.with_day(1)
|
||||
.unwrap()
|
||||
- chrono::Duration::days(1);
|
||||
Self {
|
||||
first_day,
|
||||
last_day,
|
||||
selected_day: None,
|
||||
hovered_day: None,
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,13 @@ use seed::prelude::*;
|
||||
|
||||
use jirs_data::WsMsg;
|
||||
|
||||
use crate::changes::{PageChanged, ReportsPageChange};
|
||||
use crate::model::{Model, Page, PageContent, ReportsPage};
|
||||
use crate::ws::board_load;
|
||||
use crate::{Msg, WebSocketChanged};
|
||||
use crate::pages::reports_page::model::ReportsPage;
|
||||
use crate::{
|
||||
changes::{PageChanged, ReportsPageChange},
|
||||
model::{Model, Page, PageContent},
|
||||
ws::board_load,
|
||||
Msg, WebSocketChanged,
|
||||
};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if let Msg::ChangePage(Page::Reports) = msg {
|
@ -5,10 +5,12 @@ use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::Issue;
|
||||
|
||||
use crate::model::{Model, PageContent, ReportsPage};
|
||||
use crate::shared::styled_icon::StyledIcon;
|
||||
use crate::shared::{inner_layout, ToNode};
|
||||
use crate::{Msg, PageChanged, ReportsPageChange};
|
||||
use crate::pages::reports_page::model::ReportsPage;
|
||||
use crate::{
|
||||
model::{Model, PageContent},
|
||||
shared::{inner_layout, styled_icon::StyledIcon, ToNode},
|
||||
Msg, PageChanged, ReportsPageChange,
|
||||
};
|
||||
|
||||
const SVG_MARGIN_X: u32 = 10;
|
||||
const SVG_DRAWABLE_HEIGHT: u32 = 300;
|
||||
@ -26,7 +28,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
let graph = this_month_graph(page, &this_month_updated);
|
||||
let list = issue_list(page, this_month_updated.as_slice());
|
||||
|
||||
let body = section![class!["top"], h1![class!["header"], "Reports"], graph, list];
|
||||
let body = section![C!["top"], h1![C!["header"], "Reports"], graph, list];
|
||||
|
||||
inner_layout(model, "reports", vec![body])
|
||||
}
|
||||
@ -138,8 +140,8 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<M
|
||||
}
|
||||
|
||||
div![
|
||||
class!["graph"],
|
||||
h5![class!["graphHeader"], "Last updated"],
|
||||
C!["graph"],
|
||||
h5![C!["graphHeader"], "Last updated"],
|
||||
svg![
|
||||
attrs![At::Height => SVG_HEIGHT, At::Width => SVG_WIDTH],
|
||||
svg_parts,
|
||||
@ -173,21 +175,21 @@ fn issue_list(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
children.push(li![
|
||||
class!["issue"],
|
||||
class![active_class],
|
||||
span![class!["priority"], priority_icon],
|
||||
span![class!["type"], type_icon],
|
||||
span![class!["name"], title.as_str()],
|
||||
C!["issue"],
|
||||
C![active_class],
|
||||
span![C!["priority"], priority_icon],
|
||||
span![C!["type"], type_icon],
|
||||
span![C!["name"], title.as_str()],
|
||||
span![
|
||||
class!["desc"],
|
||||
C!["desc"],
|
||||
description.as_ref().cloned().unwrap_or_default()
|
||||
],
|
||||
span![class!["updatedAt"], day.as_str()],
|
||||
span![C!["updatedAt"], day.as_str()],
|
||||
]);
|
||||
}
|
||||
div![
|
||||
class!["issueList"],
|
||||
h5![class!["issueListHeader"], "Issues this month"],
|
||||
C!["issueList"],
|
||||
h5![C!["issueListHeader"], "Issues this month"],
|
||||
children
|
||||
]
|
||||
}
|
6
jirs-client/src/pages/sign_in_page/mod.rs
Normal file
6
jirs-client/src/pages/sign_in_page/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
pub mod model;
|
||||
pub mod update;
|
||||
pub mod view;
|
12
jirs-client/src/pages/sign_in_page/model.rs
Normal file
12
jirs-client/src/pages/sign_in_page/model.rs
Normal file
@ -0,0 +1,12 @@
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SignInPage {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub token: String,
|
||||
pub login_success: bool,
|
||||
pub bad_token: String,
|
||||
// touched
|
||||
pub username_touched: bool,
|
||||
pub email_touched: bool,
|
||||
pub token_touched: bool,
|
||||
}
|
83
jirs-client/src/pages/sign_in_page/update.rs
Normal file
83
jirs-client/src/pages/sign_in_page/update.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use seed::{prelude::*, *};
|
||||
use uuid::Uuid;
|
||||
|
||||
use jirs_data::{SignInFieldId, WsMsg};
|
||||
|
||||
use crate::pages::sign_in_page::model::SignInPage;
|
||||
use crate::{
|
||||
model::{self, Model, Page, PageContent},
|
||||
shared::write_auth_token,
|
||||
ws::send_ws_msg,
|
||||
FieldId, Msg, WebSocketChanged,
|
||||
};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.page != Page::SignIn {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Msg::ChangePage(Page::SignIn) = msg {
|
||||
build_page_content(model);
|
||||
return;
|
||||
};
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::SignIn(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Username), value) => {
|
||||
page.username = value;
|
||||
page.username_touched = true;
|
||||
}
|
||||
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Email), value) => {
|
||||
page.email = value;
|
||||
page.email_touched = true;
|
||||
}
|
||||
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Token), value) => {
|
||||
page.token = value;
|
||||
page.token_touched = true;
|
||||
}
|
||||
Msg::SignInRequest => {
|
||||
send_ws_msg(
|
||||
WsMsg::AuthenticateRequest(page.email.clone(), page.username.clone()),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::BindClientRequest => {
|
||||
let bind_token: uuid::Uuid = match Uuid::from_str(page.token.as_str()) {
|
||||
Ok(token) => token,
|
||||
Err(error) => {
|
||||
error!(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
send_ws_msg(WsMsg::BindTokenCheck(bind_token), model.ws.as_ref(), orders);
|
||||
}
|
||||
Msg::WebSocketChange(change) => match change {
|
||||
WebSocketChanged::WsMsg(WsMsg::AuthenticateSuccess) => {
|
||||
page.login_success = true;
|
||||
}
|
||||
WebSocketChanged::WsMsg(WsMsg::BindTokenOk(access_token)) => {
|
||||
match write_auth_token(Some(access_token)) {
|
||||
Ok(msg) => {
|
||||
orders.skip().send_msg(msg);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
fn build_page_content(model: &mut Model) {
|
||||
model.page_content = PageContent::SignIn(Box::new(SignInPage::default()));
|
||||
}
|
@ -1,90 +1,20 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use seed::{prelude::*, *};
|
||||
use uuid::Uuid;
|
||||
|
||||
use jirs_data::WsMsg;
|
||||
|
||||
use crate::model::{Model, Page, PageContent, SignInPage};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_link::StyledLink;
|
||||
use crate::shared::{outer_layout, write_auth_token, ToNode};
|
||||
use crate::validations::{is_email, is_token};
|
||||
use crate::ws::send_ws_msg;
|
||||
use crate::{model, FieldId, Msg, SignInFieldId, WebSocketChanged};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.page != Page::SignIn {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Msg::ChangePage(Page::SignIn) = msg {
|
||||
build_page_content(model);
|
||||
return;
|
||||
};
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::SignIn(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Username), value) => {
|
||||
page.username = value;
|
||||
page.username_touched = true;
|
||||
}
|
||||
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Email), value) => {
|
||||
page.email = value;
|
||||
page.email_touched = true;
|
||||
}
|
||||
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Token), value) => {
|
||||
page.token = value;
|
||||
page.token_touched = true;
|
||||
}
|
||||
Msg::SignInRequest => {
|
||||
send_ws_msg(
|
||||
WsMsg::AuthenticateRequest(page.email.clone(), page.username.clone()),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::BindClientRequest => {
|
||||
let bind_token: uuid::Uuid = match Uuid::from_str(page.token.as_str()) {
|
||||
Ok(token) => token,
|
||||
Err(error) => {
|
||||
error!(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
send_ws_msg(WsMsg::BindTokenCheck(bind_token), model.ws.as_ref(), orders);
|
||||
}
|
||||
Msg::WebSocketChange(change) => match change {
|
||||
WebSocketChanged::WsMsg(WsMsg::AuthenticateSuccess) => {
|
||||
page.login_success = true;
|
||||
}
|
||||
WebSocketChanged::WsMsg(WsMsg::BindTokenOk(access_token)) => {
|
||||
match write_auth_token(Some(access_token)) {
|
||||
Ok(msg) => {
|
||||
orders.skip().send_msg(msg);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
fn build_page_content(model: &mut Model) {
|
||||
model.page_content = PageContent::SignIn(Box::new(SignInPage::default()));
|
||||
}
|
||||
use crate::{
|
||||
model::{self, PageContent},
|
||||
shared::{
|
||||
outer_layout,
|
||||
styled_button::StyledButton,
|
||||
styled_field::StyledField,
|
||||
styled_form::StyledForm,
|
||||
styled_icon::{Icon, StyledIcon},
|
||||
styled_input::StyledInput,
|
||||
styled_link::StyledLink,
|
||||
ToNode,
|
||||
},
|
||||
validations::{is_email, is_token},
|
||||
FieldId, Msg, SignInFieldId,
|
||||
};
|
||||
|
||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
@ -133,7 +63,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
let submit_field = StyledField::build()
|
||||
.input(div![class!["twoRow"], submit, register_link,])
|
||||
.input(div![C!["twoRow"], submit, register_link,])
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
@ -144,7 +74,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
.into_node();
|
||||
|
||||
let no_pass_section = div![
|
||||
class!["noPasswordSection"],
|
||||
C!["noPasswordSection"],
|
||||
attrs![At::Title => "We don't believe password is helping anyone. Instead after user provide correct login and e-mail he'll receive mail with 1-use token."],
|
||||
help_icon,
|
||||
span!["Why I don't see password?"]
|
6
jirs-client/src/pages/sign_up_page/mod.rs
Normal file
6
jirs-client/src/pages/sign_up_page/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
pub mod model;
|
||||
pub mod update;
|
||||
pub mod view;
|
10
jirs-client/src/pages/sign_up_page/model.rs
Normal file
10
jirs-client/src/pages/sign_up_page/model.rs
Normal file
@ -0,0 +1,10 @@
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SignUpPage {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub sign_up_success: bool,
|
||||
pub error: String,
|
||||
// touched
|
||||
pub username_touched: bool,
|
||||
pub email_touched: bool,
|
||||
}
|
58
jirs-client/src/pages/sign_up_page/update.rs
Normal file
58
jirs-client/src/pages/sign_up_page/update.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::{SignUpFieldId, WsMsg};
|
||||
|
||||
use crate::pages::sign_up_page::model::SignUpPage;
|
||||
use crate::{
|
||||
model::{self, Model, Page, PageContent},
|
||||
ws::send_ws_msg,
|
||||
FieldId, Msg, WebSocketChanged,
|
||||
};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.page != Page::SignUp {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Msg::ChangePage(Page::SignUp) = msg {
|
||||
build_page_content(model);
|
||||
return;
|
||||
};
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::SignUp(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::StrInputChanged(FieldId::SignUp(SignUpFieldId::Username), value) => {
|
||||
page.username = value;
|
||||
page.username_touched = true;
|
||||
}
|
||||
Msg::StrInputChanged(FieldId::SignUp(SignUpFieldId::Email), value) => {
|
||||
page.email = value;
|
||||
page.email_touched = true;
|
||||
}
|
||||
Msg::SignUpRequest => {
|
||||
send_ws_msg(
|
||||
WsMsg::SignUpRequest(page.email.clone(), page.username.clone()),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::WebSocketChange(change) => match change {
|
||||
WebSocketChanged::WsMsg(WsMsg::SignUpSuccess) => {
|
||||
page.sign_up_success = true;
|
||||
}
|
||||
WebSocketChanged::WsMsg(WsMsg::SignUpPairTaken) => {
|
||||
page.error = "Pair you give is either taken or is not matching".to_string();
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_page_content(model: &mut Model) {
|
||||
model.page_content = PageContent::SignUp(Box::new(SignUpPage::default()));
|
||||
}
|
@ -1,66 +1,22 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{SignUpFieldId, WsMsg};
|
||||
use jirs_data::SignUpFieldId;
|
||||
|
||||
use crate::model::{Model, Page, PageContent, SignUpPage};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_link::StyledLink;
|
||||
use crate::shared::{outer_layout, ToNode};
|
||||
use crate::validations::is_email;
|
||||
use crate::ws::send_ws_msg;
|
||||
use crate::{model, FieldId, Msg, WebSocketChanged};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.page != Page::SignUp {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Msg::ChangePage(Page::SignUp) = msg {
|
||||
build_page_content(model);
|
||||
return;
|
||||
};
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::SignUp(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::StrInputChanged(FieldId::SignUp(SignUpFieldId::Username), value) => {
|
||||
page.username = value;
|
||||
page.username_touched = true;
|
||||
}
|
||||
Msg::StrInputChanged(FieldId::SignUp(SignUpFieldId::Email), value) => {
|
||||
page.email = value;
|
||||
page.email_touched = true;
|
||||
}
|
||||
Msg::SignUpRequest => {
|
||||
send_ws_msg(
|
||||
WsMsg::SignUpRequest(page.email.clone(), page.username.clone()),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::WebSocketChange(change) => match change {
|
||||
WebSocketChanged::WsMsg(WsMsg::SignUpSuccess) => {
|
||||
page.sign_up_success = true;
|
||||
}
|
||||
WebSocketChanged::WsMsg(WsMsg::SignUpPairTaken) => {
|
||||
page.error = "Pair you give is either taken or is not matching".to_string();
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_page_content(model: &mut Model) {
|
||||
model.page_content = PageContent::SignUp(Box::new(SignUpPage::default()));
|
||||
}
|
||||
use crate::{
|
||||
model::{self, PageContent},
|
||||
shared::{
|
||||
outer_layout,
|
||||
styled_button::StyledButton,
|
||||
styled_field::StyledField,
|
||||
styled_form::StyledForm,
|
||||
styled_icon::{Icon, StyledIcon},
|
||||
styled_input::StyledInput,
|
||||
styled_link::StyledLink,
|
||||
ToNode,
|
||||
},
|
||||
validations::is_email,
|
||||
FieldId, Msg,
|
||||
};
|
||||
|
||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
@ -111,7 +67,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
.into_node();
|
||||
|
||||
let submit_field = StyledField::build()
|
||||
.input(div![class!["twoRow"], submit, sign_in_link,])
|
||||
.input(div![C!["twoRow"], submit, sign_in_link,])
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
@ -122,7 +78,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
.into_node();
|
||||
|
||||
let no_pass_section = div![
|
||||
class!["noPasswordSection"],
|
||||
C!["noPasswordSection"],
|
||||
attrs![At::Title => "We don't believe password is helping anyone. Instead after user provide correct login and e-mail he'll receive mail with 1-use token."],
|
||||
help_icon,
|
||||
span!["Why I don't see password?"]
|
||||
@ -131,7 +87,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
let error_row = if page.error.is_empty() {
|
||||
empty![]
|
||||
} else {
|
||||
div![class!["error"], p![page.error.as_str()]]
|
||||
div![C!["error"], p![page.error.as_str()]]
|
||||
};
|
||||
|
||||
let sign_up_form = StyledForm::build()
|
6
jirs-client/src/pages/users_page/mod.rs
Normal file
6
jirs-client/src/pages/users_page/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
pub mod model;
|
||||
pub mod update;
|
||||
pub mod view;
|
40
jirs-client/src/pages/users_page/model.rs
Normal file
40
jirs-client/src/pages/users_page/model.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use jirs_data::{Invitation, User, UserRole, UsersFieldId};
|
||||
|
||||
use crate::model::InvitationFormState;
|
||||
use crate::shared::styled_select::StyledSelectState;
|
||||
use crate::FieldId;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UsersPage {
|
||||
pub name: String,
|
||||
pub name_touched: bool,
|
||||
pub email: String,
|
||||
pub email_touched: bool,
|
||||
pub user_role: UserRole,
|
||||
|
||||
pub user_role_state: StyledSelectState,
|
||||
pub pending_invitations: Vec<String>,
|
||||
pub error: String,
|
||||
pub form_state: InvitationFormState,
|
||||
|
||||
pub invited_users: Vec<User>,
|
||||
pub invitations: Vec<Invitation>,
|
||||
}
|
||||
|
||||
impl Default for UsersPage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "".to_string(),
|
||||
name_touched: false,
|
||||
email: "".to_string(),
|
||||
email_touched: false,
|
||||
user_role: Default::default(),
|
||||
user_role_state: StyledSelectState::new(FieldId::Users(UsersFieldId::UserRole), vec![]),
|
||||
pending_invitations: vec![],
|
||||
error: "".to_string(),
|
||||
form_state: Default::default(),
|
||||
invited_users: vec![],
|
||||
invitations: vec![],
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,13 @@ use seed::prelude::Orders;
|
||||
|
||||
use jirs_data::{InvitationState, UserRole, UsersFieldId, WsMsg};
|
||||
|
||||
use crate::model::{InvitationFormState, Model, Page, PageContent, UsersPage};
|
||||
use crate::shared::styled_select::StyledSelectChanged;
|
||||
use crate::ws::{invitation_load, send_ws_msg};
|
||||
use crate::{FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged};
|
||||
use crate::pages::users_page::model::UsersPage;
|
||||
use crate::{
|
||||
model::{InvitationFormState, Model, Page, PageContent},
|
||||
shared::styled_select::StyledSelectChanged,
|
||||
ws::{invitation_load, send_ws_msg},
|
||||
FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged,
|
||||
};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
if let Msg::ChangePage(Page::Users) = msg {
|
@ -2,15 +2,16 @@ use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{InvitationState, ToVec, UserRole, UsersFieldId};
|
||||
|
||||
use crate::model::{InvitationFormState, Model, PageContent};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_select::StyledSelect;
|
||||
use crate::shared::{inner_layout, ToChild, ToNode};
|
||||
use crate::validations::is_email;
|
||||
use crate::{FieldId, Msg, PageChanged, UsersPageChange};
|
||||
use crate::{
|
||||
model::{InvitationFormState, Model, PageContent},
|
||||
shared::{
|
||||
inner_layout, styled_button::StyledButton, styled_field::StyledField,
|
||||
styled_form::StyledForm, styled_input::StyledInput, styled_select::StyledSelect, ToChild,
|
||||
ToNode,
|
||||
},
|
||||
validations::is_email,
|
||||
FieldId, Msg, PageChanged, UsersPageChange,
|
||||
};
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
if model.user.is_none() {
|
||||
@ -79,11 +80,11 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
.text("Reset")
|
||||
.build()
|
||||
.into_node(),
|
||||
InvitationFormState::Failed => div![class!["error"], "There was an error"],
|
||||
InvitationFormState::Failed => div![C!["error"], "There was an error"],
|
||||
_ => empty![],
|
||||
};
|
||||
let submit_field = StyledField::build()
|
||||
.input(div![class!["invitationActions"], submit, submit_supplement])
|
||||
.input(div![C!["invitationActions"], submit, submit_supplement])
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
@ -120,7 +121,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
.unwrap_or_default();
|
||||
|
||||
li![
|
||||
class!["user"],
|
||||
C!["user"],
|
||||
span![user.name.as_str()],
|
||||
span![user.email.as_str()],
|
||||
span![format!("{}", role)],
|
||||
@ -130,9 +131,9 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
.collect();
|
||||
|
||||
let users_section = section![
|
||||
class!["usersSection"],
|
||||
h1![class!["heading"], "Users"],
|
||||
ul![class!["usersList"], users],
|
||||
C!["usersSection"],
|
||||
h1![C!["heading"], "Users"],
|
||||
ul![C!["usersList"], users],
|
||||
];
|
||||
|
||||
let invitations: Vec<Node<Msg>> = page
|
||||
@ -147,7 +148,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
li![
|
||||
class!["invitation"],
|
||||
C!["invitation"],
|
||||
attrs![At::Class => format!("{}", invitation.state)],
|
||||
span![invitation.name.as_str()],
|
||||
span![invitation.email.as_str()],
|
||||
@ -158,9 +159,9 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
.collect();
|
||||
|
||||
let invitations_section = section![
|
||||
class!["invitationsSection"],
|
||||
h1![class!["heading"], "Invitations"],
|
||||
ul![class!["invitationsList"], invitations],
|
||||
C!["invitationsSection"],
|
||||
h1![C!["heading"], "Invitations"],
|
||||
ul![C!["invitationsList"], invitations],
|
||||
];
|
||||
|
||||
inner_layout(
|
@ -1,5 +0,0 @@
|
||||
pub use update::update;
|
||||
pub use view::view;
|
||||
|
||||
mod update;
|
||||
mod view;
|
@ -1,5 +0,0 @@
|
||||
pub use update::update;
|
||||
pub use view::view;
|
||||
|
||||
mod update;
|
||||
mod view;
|
@ -1,5 +0,0 @@
|
||||
pub use update::update;
|
||||
pub use view::view;
|
||||
|
||||
mod update;
|
||||
mod view;
|
@ -1,15 +1,20 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{UserRole, WsMsg};
|
||||
|
||||
use crate::model::{Model, Page};
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::{divider, ToNode};
|
||||
use crate::ws::enqueue_ws_msg;
|
||||
use crate::{Msg, WebSocketChanged};
|
||||
use {
|
||||
crate::{
|
||||
model::{Model, Page},
|
||||
shared::{
|
||||
divider,
|
||||
styled_icon::{Icon, StyledIcon},
|
||||
ToNode,
|
||||
},
|
||||
ws::enqueue_ws_msg,
|
||||
Msg, OperationKind, ResourceKind,
|
||||
},
|
||||
jirs_data::{UserRole, WsMsg},
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
if let Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(Ok(_)))) = msg {
|
||||
if let Msg::ResourceChanged(ResourceKind::Auth, OperationKind::SingleLoaded, _) = msg {
|
||||
enqueue_ws_msg(
|
||||
vec![
|
||||
WsMsg::UserProjectsLoad,
|
||||
@ -20,86 +25,104 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
orders.skip();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(model: &Model) -> Node<Msg> {
|
||||
let project_icon = Node::from_html(include_str!("../../static/project-avatar.svg"));
|
||||
let project_icon = crate::images::project_avatar::render();
|
||||
|
||||
let project_info = match model.project.as_ref() {
|
||||
Some(project) => li![
|
||||
id!["projectInfo"],
|
||||
project_icon,
|
||||
div![
|
||||
class!["projectTexts"],
|
||||
div![class!["projectName"], project.name.as_str()],
|
||||
div![class!["projectCategory"], project.category.to_string()]
|
||||
C!["projectTexts"],
|
||||
div![C!["projectName"], project.name.as_str()],
|
||||
div![C!["projectCategory"], project.category.to_string()]
|
||||
],
|
||||
],
|
||||
_ => li![
|
||||
id!["projectInfo"],
|
||||
div![
|
||||
class!["projectTexts"],
|
||||
div![class!["projectName"], ""],
|
||||
div![class!["projectCategory"], ""]
|
||||
C!["projectTexts"],
|
||||
div![C!["projectName"], ""],
|
||||
div![C!["projectCategory"], ""]
|
||||
],
|
||||
],
|
||||
};
|
||||
let mut links = vec![];
|
||||
|
||||
if model.current_user_role() > UserRole::User {
|
||||
links.push(sidebar_link_item(
|
||||
model,
|
||||
"Project settings",
|
||||
Icon::Settings,
|
||||
Some(Page::ProjectSettings),
|
||||
));
|
||||
}
|
||||
|
||||
links.extend(vec![
|
||||
li![divider()],
|
||||
sidebar_link_item(model, "Releases", Icon::Shipping, None),
|
||||
sidebar_link_item(model, "Issue and Filters", Icon::Issues, None),
|
||||
sidebar_link_item(model, "Pages", Icon::Page, None),
|
||||
sidebar_link_item(model, "Reports", Icon::Reports, Some(Page::Reports)),
|
||||
sidebar_link_item(model, "Components", Icon::Component, None),
|
||||
]);
|
||||
|
||||
if model.current_user_role() > UserRole::User {
|
||||
links.push(sidebar_link_item(
|
||||
model,
|
||||
"Users",
|
||||
Icon::Cop,
|
||||
Some(Page::Users),
|
||||
));
|
||||
}
|
||||
|
||||
nav![
|
||||
id!["sidebar"],
|
||||
ul![
|
||||
project_info,
|
||||
sidebar_link_item(model, "Kanban Board", Icon::Board, Some(Page::Project)),
|
||||
links,
|
||||
project_settings(model),
|
||||
li![divider()],
|
||||
sidebar_link_item(model, "Releases", Icon::Shipping, None),
|
||||
sidebar_link_item(model, "Issue and Filters", Icon::Issues, None),
|
||||
sidebar_link_item(model, "Pages", Icon::Page, None),
|
||||
sidebar_link_item(model, "Reports", Icon::Reports, Some(Page::Reports)),
|
||||
sidebar_link_item(model, "Components", Icon::Component, None),
|
||||
users_link(model)
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn project_settings(model: &Model) -> Node<Msg> {
|
||||
if model.current_user_role() <= UserRole::User {
|
||||
return Node::Empty;
|
||||
}
|
||||
|
||||
sidebar_link_item(
|
||||
model,
|
||||
"Project settings",
|
||||
Icon::Settings,
|
||||
Some(Page::ProjectSettings),
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn users_link(model: &Model) -> Node<Msg> {
|
||||
if model.current_user_role() <= UserRole::User {
|
||||
return Node::Empty;
|
||||
}
|
||||
|
||||
sidebar_link_item(model, "Users", Icon::Cop, Some(Page::Users))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>) -> Node<Msg> {
|
||||
let path = page.map(|ref p| p.to_path()).unwrap_or_default();
|
||||
let mut class_list = vec![];
|
||||
if page.is_none() {
|
||||
class_list.push("notAllowed");
|
||||
let allow_flag = if page.is_none() {
|
||||
Some(C!["notAllowed"])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if Some(model.page) == page {
|
||||
class_list.push("active");
|
||||
}
|
||||
let active_flag = page.filter(|p| *p == model.page).map(|_| C!["active"]);
|
||||
let icon_node = StyledIcon::build(icon).build().into_node();
|
||||
let on_click = page.map(|p| {
|
||||
mouse_ev("click", move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
seed::Url::new()
|
||||
.set_path(p.to_path().split('/').filter(|s| !s.is_empty()))
|
||||
.go_and_push();
|
||||
Msg::ChangePage(p)
|
||||
})
|
||||
});
|
||||
|
||||
li![
|
||||
class!["linkItem"],
|
||||
class![icon.to_str()],
|
||||
C!["linkItem"],
|
||||
active_flag,
|
||||
allow_flag,
|
||||
C![icon.to_str()],
|
||||
a![
|
||||
attrs![At::Href => path],
|
||||
on_click,
|
||||
icon_node,
|
||||
div![attrs![At::Class => "linkText"], name],
|
||||
div![C!["linkText"], name],
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::model::Model;
|
||||
use crate::model::Page;
|
||||
use crate::Msg;
|
||||
use crate::{
|
||||
model::{Model, Page},
|
||||
resolve_page, Msg,
|
||||
};
|
||||
|
||||
pub mod aside;
|
||||
pub mod drag;
|
||||
@ -37,21 +36,28 @@ pub trait ToChild<'l> {
|
||||
fn to_child<'m: 'l>(&'m self) -> Self::Builder;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn go_to_board(orders: &mut impl Orders<Msg>) {
|
||||
go_to("/board");
|
||||
go_to("board", orders);
|
||||
orders.skip().send_msg(Msg::ChangePage(Page::Project));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn go_to_login(orders: &mut impl Orders<Msg>) {
|
||||
go_to("/login");
|
||||
go_to("login", orders);
|
||||
orders.skip().send_msg(Msg::ChangePage(Page::SignIn));
|
||||
}
|
||||
|
||||
pub fn go_to(url: &str) {
|
||||
seed::push_route(Url::from_str(url).unwrap());
|
||||
#[inline]
|
||||
pub fn go_to(url: &str, orders: &mut impl Orders<Msg>) {
|
||||
let url = seed::Url::new().add_path_part(url);
|
||||
url.go_and_push();
|
||||
if let Some(page) = resolve_page(url) {
|
||||
orders.skip().send_msg(Msg::ChangePage(page));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_issue<'l>(model: &'l Model, issue_id: IssueId) -> Option<&'l Issue> {
|
||||
pub fn find_issue(model: &'_ Model, issue_id: IssueId) -> Option<&'_ Issue> {
|
||||
model.issues.iter().find(|issue| issue.id == issue_id)
|
||||
}
|
||||
|
||||
@ -60,14 +66,14 @@ pub trait ToNode {
|
||||
}
|
||||
|
||||
pub fn divider() -> Node<Msg> {
|
||||
div![class!["divider"], ""]
|
||||
div![C!["divider"], ""]
|
||||
}
|
||||
|
||||
pub fn inner_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
|
||||
let modal_node = crate::modal::view(model);
|
||||
article![
|
||||
modal_node,
|
||||
class!["inner-layout", "innerPage"],
|
||||
C!["inner-layout", "innerPage"],
|
||||
id![page_name],
|
||||
navbar_left::render(model),
|
||||
aside::render(model),
|
||||
@ -78,7 +84,7 @@ pub fn inner_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) ->
|
||||
pub fn outer_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
|
||||
let modal = crate::modal::view(model);
|
||||
article![
|
||||
class!["outer-layout", "outerPage"],
|
||||
C!["outer-layout", "outerPage"],
|
||||
id![page_name],
|
||||
modal,
|
||||
children
|
||||
|
@ -2,13 +2,18 @@ use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{InvitationToken, Message, MessageType, WsMsg};
|
||||
|
||||
use crate::model::Model;
|
||||
use crate::shared::styled_avatar::StyledAvatar;
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::{divider, styled_tooltip, ToNode};
|
||||
use crate::ws::send_ws_msg;
|
||||
use crate::Msg;
|
||||
use crate::{
|
||||
model::Model,
|
||||
shared::{
|
||||
divider,
|
||||
styled_avatar::StyledAvatar,
|
||||
styled_button::StyledButton,
|
||||
styled_icon::{Icon, StyledIcon},
|
||||
styled_tooltip, ToNode,
|
||||
},
|
||||
ws::send_ws_msg,
|
||||
Msg, Page,
|
||||
};
|
||||
|
||||
trait IntoNavItemIcon {
|
||||
fn into_nav_item_icon(self) -> Node<Msg>;
|
||||
@ -44,7 +49,7 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
|
||||
|
||||
let user_icon = match model.user.as_ref() {
|
||||
Some(user) => i![
|
||||
class!["styledIcon"],
|
||||
C!["styledIcon"],
|
||||
StyledAvatar::build()
|
||||
.size(27)
|
||||
.name(user.name.as_str())
|
||||
@ -77,6 +82,12 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
|
||||
navbar_left_item("Create Issue", Icon::Plus, Some("/add-issue"), None),
|
||||
]
|
||||
};
|
||||
let go_to_profile = mouse_ev("click", move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
seed::Url::new().add_path_part("profile").go_and_push();
|
||||
Msg::ChangePage(Page::Profile)
|
||||
});
|
||||
|
||||
vec![
|
||||
about_tooltip_popup(model),
|
||||
@ -84,14 +95,14 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
|
||||
aside![
|
||||
id!["navbar-left"],
|
||||
a![
|
||||
class!["logoLink"],
|
||||
C!["logoLink"],
|
||||
attrs![At::Href => "/"],
|
||||
div![class!["styledLogo"], logo_svg]
|
||||
div![C!["styledLogo"], logo_svg]
|
||||
],
|
||||
issue_nav,
|
||||
div![
|
||||
class!["bottom"],
|
||||
navbar_left_item("Profile", user_icon, Some("/profile"), None),
|
||||
C!["bottom"],
|
||||
navbar_left_item("Profile", user_icon, Some("/profile"), Some(go_to_profile)),
|
||||
messages,
|
||||
about_tooltip(model, navbar_left_item("About", Icon::Help, None, None)),
|
||||
],
|
||||
@ -99,6 +110,7 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
|
||||
]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn navbar_left_item<I>(
|
||||
text: &str,
|
||||
icon: I,
|
||||
@ -109,13 +121,12 @@ where
|
||||
I: IntoNavItemIcon,
|
||||
{
|
||||
let styled_icon = icon.into_nav_item_icon();
|
||||
let href = href.unwrap_or_else(|| "#");
|
||||
|
||||
a![
|
||||
class!["item"],
|
||||
attrs![At::Href => href],
|
||||
C!["item"],
|
||||
attrs![At::Href => href.unwrap_or("#")],
|
||||
styled_icon,
|
||||
span![class!["itemText"], text],
|
||||
span![C!["itemText"], text],
|
||||
on_click,
|
||||
]
|
||||
}
|
||||
@ -124,7 +135,7 @@ pub fn about_tooltip(_model: &Model, children: Node<Msg>) -> Node<Msg> {
|
||||
let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| {
|
||||
Some(Msg::ToggleTooltip(styled_tooltip::Variant::About))
|
||||
});
|
||||
div![class!["aboutTooltip"], on_click, children]
|
||||
div![C!["aboutTooltip"], on_click, children]
|
||||
}
|
||||
|
||||
fn messages_tooltip_popup(model: &Model) -> Node<Msg> {
|
||||
@ -140,7 +151,7 @@ fn messages_tooltip_popup(model: &Model) -> Node<Msg> {
|
||||
}
|
||||
};
|
||||
}
|
||||
let body = div![on_click, class!["messagesList"], messages];
|
||||
let body = div![on_click, C!["messagesList"], messages];
|
||||
styled_tooltip::StyledTooltip::build()
|
||||
.add_class("messagesPopup")
|
||||
.visible(model.messages_tooltip_visible)
|
||||
@ -166,9 +177,9 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
|
||||
} else {
|
||||
let link_icon = StyledIcon::build(Icon::Link).build().into_node();
|
||||
div![
|
||||
class!["hyperlink"],
|
||||
C!["hyperlink"],
|
||||
a![
|
||||
class!["styledLink"],
|
||||
C!["styledLink"],
|
||||
attrs![At::Href => hyper_link],
|
||||
link_icon,
|
||||
hyper_link
|
||||
@ -188,9 +199,9 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
|
||||
.build()
|
||||
.into_node();
|
||||
let top = div![
|
||||
class!["top"],
|
||||
div![class!["summary"], summary],
|
||||
div![class!["action"], close_button],
|
||||
C!["top"],
|
||||
div![C!["summary"], summary],
|
||||
div![C!["action"], close_button],
|
||||
];
|
||||
|
||||
let node = match message_type {
|
||||
@ -221,23 +232,23 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
|
||||
.build()
|
||||
.into_node();
|
||||
div![
|
||||
class!["message"],
|
||||
C!["message"],
|
||||
attrs![At::Class => format!("{}", message_type)],
|
||||
top,
|
||||
div![class!["description"], message_description],
|
||||
div![class!["actions"], accept, reject],
|
||||
div![C!["description"], message_description],
|
||||
div![C!["actions"], accept, reject],
|
||||
]
|
||||
}
|
||||
MessageType::AssignedToIssue => div![
|
||||
class!["message assignedToIssue"],
|
||||
C!["message assignedToIssue"],
|
||||
top,
|
||||
div![class!["description"], message_description],
|
||||
div![C!["description"], message_description],
|
||||
hyperlink,
|
||||
],
|
||||
MessageType::Mention => div![
|
||||
class!["message mention"],
|
||||
C!["message mention"],
|
||||
top,
|
||||
div![class!["description"], message_description],
|
||||
div![C!["description"], message_description],
|
||||
hyperlink,
|
||||
],
|
||||
};
|
||||
@ -262,18 +273,18 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
|
||||
});
|
||||
let body = div![
|
||||
on_click,
|
||||
class!["feedbackDropdown"],
|
||||
C!["feedbackDropdown"],
|
||||
div![
|
||||
class!["feedbackImageCont"],
|
||||
C!["feedbackImageCont"],
|
||||
img![attrs![At::Src => "/feedback.png"]],
|
||||
class!["feedbackImage"],
|
||||
C!["feedbackImage"],
|
||||
],
|
||||
div![
|
||||
class!["feedbackParagraph"],
|
||||
C!["feedbackParagraph"],
|
||||
"This simplified Jira clone is built with Seed.rs on the front-end and Actix-Web on the back-end."
|
||||
],
|
||||
div![
|
||||
class!["feedbackParagraph"],
|
||||
C!["feedbackParagraph"],
|
||||
"Read more on my website or reach out via ",
|
||||
a![
|
||||
attrs![At::Href => "mailto:adrian.wozniak@ita-prog.pl"],
|
||||
@ -326,7 +337,7 @@ fn parse_description(model: &Model, desc: &str) -> Node<Msg> {
|
||||
.size(16)
|
||||
.build()
|
||||
.into_node();
|
||||
span![class!["mention"], avatar, user.name.as_str()]
|
||||
span![C!["mention"], avatar, user.name.as_str()]
|
||||
})
|
||||
.unwrap_or_else(|| span![word]);
|
||||
container.add_child(child).add_text(" ");
|
||||
|
@ -1,7 +1,7 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::shared::ToNode;
|
||||
use crate::Msg;
|
||||
use {
|
||||
crate::{shared::ToNode, Msg},
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
pub struct StyledAvatar<'l> {
|
||||
avatar_url: Option<&'l str>,
|
||||
@ -26,6 +26,7 @@ impl<'l> Default for StyledAvatar<'l> {
|
||||
}
|
||||
|
||||
impl<'l> StyledAvatar<'l> {
|
||||
#[inline(always)]
|
||||
pub fn build() -> StyledAvatarBuilder<'l> {
|
||||
StyledAvatarBuilder {
|
||||
avatar_url: None,
|
||||
@ -39,6 +40,7 @@ impl<'l> StyledAvatar<'l> {
|
||||
}
|
||||
|
||||
impl<'l> ToNode for StyledAvatar<'l> {
|
||||
#[inline(always)]
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
}
|
||||
@ -54,6 +56,7 @@ pub struct StyledAvatarBuilder<'l> {
|
||||
}
|
||||
|
||||
impl<'l> StyledAvatarBuilder<'l> {
|
||||
#[inline(always)]
|
||||
pub fn avatar_url<'m: 'l>(mut self, avatar_url: &'m str) -> Self {
|
||||
if !avatar_url.is_empty() {
|
||||
self.avatar_url = Some(avatar_url);
|
||||
@ -61,31 +64,37 @@ impl<'l> StyledAvatarBuilder<'l> {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn size(mut self, size: u32) -> Self {
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn name<'m: 'l>(mut self, name: &'m str) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn on_click(mut self, on_click: EventHandler<Msg>) -> Self {
|
||||
self.on_click = Some(on_click);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self {
|
||||
self.class_list.push(name);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn user_index(mut self, user_index: usize) -> Self {
|
||||
self.user_index = user_index;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn build(self) -> StyledAvatar<'l> {
|
||||
StyledAvatar {
|
||||
avatar_url: self.avatar_url,
|
||||
@ -111,11 +120,10 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
||||
let index = user_index % 8;
|
||||
|
||||
let shared_style = format!("width: {size}px; height: {size}px", size = size);
|
||||
let handler = match on_click {
|
||||
None => vec![],
|
||||
Some(h) => vec![h],
|
||||
let class_list: Attrs = {
|
||||
let s: String = class_list.join(" ");
|
||||
C![s.as_str()]
|
||||
};
|
||||
let class_list: Vec<Attrs> = class_list.into_iter().map(|s| class![s]).collect();
|
||||
let letter = name
|
||||
.chars()
|
||||
.rev()
|
||||
@ -130,11 +138,10 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
||||
url = url
|
||||
);
|
||||
div![
|
||||
class!["styledAvatar"],
|
||||
class!["image"],
|
||||
C!["styledAvatar image"],
|
||||
class_list,
|
||||
attrs![At::Style => style, At::Title => name],
|
||||
handler
|
||||
on_click
|
||||
]
|
||||
}
|
||||
_ => {
|
||||
@ -144,15 +151,14 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
||||
size = size
|
||||
);
|
||||
div![
|
||||
class!["styledAvatar"],
|
||||
class!["letter"],
|
||||
C!["styledAvatar letter"],
|
||||
class_list,
|
||||
attrs![
|
||||
At::Class => format!("avatarColor{}", index + 1),
|
||||
At::Style => style
|
||||
],
|
||||
span![letter],
|
||||
handler,
|
||||
on_click,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::shared::ToNode;
|
||||
use crate::{ButtonId, Msg};
|
||||
use {
|
||||
crate::{shared::ToNode, ButtonId, Msg},
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
enum Variant {
|
||||
@ -45,27 +45,33 @@ pub struct StyledButtonBuilder<'l> {
|
||||
}
|
||||
|
||||
impl<'l> StyledButtonBuilder<'l> {
|
||||
#[inline(always)]
|
||||
fn variant(mut self, value: Variant) -> Self {
|
||||
self.variant = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn primary(self) -> Self {
|
||||
self.variant(Variant::Primary)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn success(self) -> Self {
|
||||
self.variant(Variant::Success)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn danger(self) -> Self {
|
||||
self.variant(Variant::Danger)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn secondary(self) -> Self {
|
||||
self.variant(Variant::Secondary)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn empty(self) -> Self {
|
||||
self.variant(Variant::Empty)
|
||||
}
|
||||
@ -75,21 +81,25 @@ impl<'l> StyledButtonBuilder<'l> {
|
||||
// self
|
||||
// }
|
||||
|
||||
#[inline(always)]
|
||||
pub fn disabled(mut self, value: bool) -> Self {
|
||||
self.disabled = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn active(mut self, value: bool) -> Self {
|
||||
self.active = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn text(mut self, value: &'l str) -> Self {
|
||||
self.text = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn icon<I>(mut self, value: I) -> Self
|
||||
where
|
||||
I: ToNode,
|
||||
@ -98,37 +108,42 @@ impl<'l> StyledButtonBuilder<'l> {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn on_click(mut self, value: EventHandler<Msg>) -> Self {
|
||||
self.on_click = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn children(mut self, value: Vec<Node<Msg>>) -> Self {
|
||||
self.children = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add_class(mut self, name: &'l str) -> Self {
|
||||
self.class_list.push(name);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set_type_reset(mut self) -> Self {
|
||||
self.button_type = Some("reset");
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn build(self) -> StyledButton<'l> {
|
||||
StyledButton {
|
||||
variant: self.variant.unwrap_or_else(|| Variant::Primary),
|
||||
disabled: self.disabled.unwrap_or_else(|| false),
|
||||
active: self.active.unwrap_or_else(|| false),
|
||||
variant: self.variant.unwrap_or(Variant::Primary),
|
||||
disabled: self.disabled.unwrap_or(false),
|
||||
active: self.active.unwrap_or(false),
|
||||
text: self.text,
|
||||
icon: self.icon,
|
||||
on_click: self.on_click,
|
||||
children: self.children.unwrap_or_default(),
|
||||
class_list: self.class_list,
|
||||
button_type: self.button_type.unwrap_or_else(|| "submit"),
|
||||
button_type: self.button_type.unwrap_or("submit"),
|
||||
button_id: self.button_id,
|
||||
}
|
||||
}
|
||||
@ -148,17 +163,20 @@ pub struct StyledButton<'l> {
|
||||
}
|
||||
|
||||
impl<'l> StyledButton<'l> {
|
||||
#[inline(always)]
|
||||
pub fn build() -> StyledButtonBuilder<'l> {
|
||||
StyledButtonBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> ToNode for StyledButton<'l> {
|
||||
#[inline(always)]
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn render(values: StyledButton) -> Node<Msg> {
|
||||
let StyledButton {
|
||||
text,
|
||||
@ -187,18 +205,21 @@ pub fn render(values: StyledButton) -> Node<Msg> {
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let icon_node = icon.unwrap_or_else(|| empty![]);
|
||||
let icon_node = icon.unwrap_or(Node::Empty);
|
||||
let content = if children.is_empty() && text.is_none() {
|
||||
empty![]
|
||||
Node::Empty
|
||||
} else {
|
||||
span![class!["text"], text.unwrap_or_default(), children]
|
||||
span![C!["text"], text.unwrap_or_default(), children]
|
||||
};
|
||||
|
||||
let class_list: Vec<Attrs> = class_list.into_iter().map(|s| class![s]).collect();
|
||||
let class_list: Attrs = {
|
||||
let class_list: String = class_list.join(" ");
|
||||
C![class_list.as_str()]
|
||||
};
|
||||
let button_id = button_id.map(|id| id.to_str()).unwrap_or_default();
|
||||
|
||||
seed::button![
|
||||
class!["styledButton"],
|
||||
C!["styledButton"],
|
||||
class_list,
|
||||
attrs![
|
||||
At::Id => button_id,
|
||||
|
@ -189,7 +189,7 @@ fn render(values: StyledCheckbox) -> Node<Msg> {
|
||||
.collect();
|
||||
|
||||
div![
|
||||
class!["styledCheckbox"],
|
||||
C!["styledCheckbox"],
|
||||
attrs![At::Class => class_list.join(" ")],
|
||||
opt,
|
||||
]
|
||||
|
@ -1,166 +1,203 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::shared::styled_textarea::StyledTextarea;
|
||||
use crate::shared::ToNode;
|
||||
use crate::{FieldChange, FieldId, Msg};
|
||||
|
||||
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
|
||||
pub enum Mode {
|
||||
Editor,
|
||||
View,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyledEditorState {
|
||||
pub mode: Mode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyledEditor {
|
||||
id: FieldId,
|
||||
text: String,
|
||||
mode: Mode,
|
||||
update_event: Ev,
|
||||
}
|
||||
|
||||
impl StyledEditor {
|
||||
pub fn build(id: FieldId) -> StyledEditorBuilder {
|
||||
StyledEditorBuilder {
|
||||
id,
|
||||
text: String::new(),
|
||||
mode: Mode::View,
|
||||
update_event: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StyledEditorBuilder {
|
||||
id: FieldId,
|
||||
text: String,
|
||||
mode: Mode,
|
||||
update_event: Option<Ev>,
|
||||
}
|
||||
|
||||
impl StyledEditorBuilder {
|
||||
pub fn text<S>(mut self, text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.text = text.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mode(mut self, mode: Mode) -> Self {
|
||||
self.mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledEditor {
|
||||
StyledEditor {
|
||||
id: self.id,
|
||||
text: self.text,
|
||||
mode: self.mode,
|
||||
update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_on(mut self, ev: Ev) -> Self {
|
||||
self.update_event = Some(ev);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ToNode for StyledEditor {
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(values: StyledEditor) -> Node<Msg> {
|
||||
let StyledEditor {
|
||||
id,
|
||||
text,
|
||||
mode,
|
||||
update_event,
|
||||
} = values;
|
||||
|
||||
let field_id = id.clone();
|
||||
let on_editor_clicked = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::TabChanged(field_id, Mode::Editor))
|
||||
});
|
||||
|
||||
let field_id = id.clone();
|
||||
let on_view_clicked = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::TabChanged(field_id, Mode::View))
|
||||
});
|
||||
|
||||
let editor_id = format!("editor-{}", id);
|
||||
let view_id = format!("view-{}", id);
|
||||
let name = format!("styled-editor-{}", id);
|
||||
|
||||
let text_area = StyledTextarea::build(id)
|
||||
.height(40)
|
||||
.update_on(update_event)
|
||||
.value(text.as_str())
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let parsed = comrak::markdown_to_html(text.as_str(), &comrak::ComrakOptions::default());
|
||||
let parsed_node = Node::from_html(parsed.as_str());
|
||||
|
||||
let (editor_radio_node, view_radio_node) = match mode {
|
||||
Mode::Editor => (
|
||||
seed::input![
|
||||
id![editor_id.as_str()],
|
||||
attrs![At::Type => "radio"; At::Name => name.as_str(); At::Class => "editorRadio"; At::Checked => true],
|
||||
],
|
||||
seed::input![
|
||||
id![view_id.as_str()],
|
||||
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio";],
|
||||
],
|
||||
),
|
||||
Mode::View => (
|
||||
seed::input![
|
||||
id![editor_id.as_str()],
|
||||
class!["editorRadio"],
|
||||
attrs![At::Type => "radio"; At::Name => name.as_str();],
|
||||
],
|
||||
seed::input![
|
||||
id![view_id.as_str()],
|
||||
class!["viewRadio"],
|
||||
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
|
||||
],
|
||||
),
|
||||
};
|
||||
|
||||
div![
|
||||
attrs![At::Class => "styledEditor"],
|
||||
label![
|
||||
if mode == Mode::View {
|
||||
class!["navbar viewTab activeTab"]
|
||||
} else {
|
||||
class!["navbar viewTab"]
|
||||
},
|
||||
attrs![At::For => view_id.as_str()],
|
||||
"View",
|
||||
on_view_clicked
|
||||
],
|
||||
label![
|
||||
if mode == Mode::Editor {
|
||||
C!["navbar editorTab activeTab"]
|
||||
} else {
|
||||
C!["navbar editorTab"]
|
||||
},
|
||||
attrs![At::For => editor_id.as_str()],
|
||||
"Editor",
|
||||
on_editor_clicked
|
||||
],
|
||||
editor_radio_node,
|
||||
text_area,
|
||||
view_radio_node,
|
||||
div![attrs![At::Class => "view"], parsed_node],
|
||||
]
|
||||
}
|
||||
use {
|
||||
crate::{
|
||||
shared::{styled_textarea::StyledTextarea, ToNode},
|
||||
FieldChange, FieldId, Msg,
|
||||
},
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
|
||||
pub enum Mode {
|
||||
Editor,
|
||||
View,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialOrd, PartialEq)]
|
||||
pub struct StyledEditorState {
|
||||
pub mode: Mode,
|
||||
pub initial_text: String,
|
||||
}
|
||||
|
||||
impl StyledEditorState {
|
||||
pub fn new<S: Into<String>>(mode: Mode, text: S) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
initial_text: text.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyledEditor {
|
||||
id: FieldId,
|
||||
initial_text: String,
|
||||
text: String,
|
||||
html: String,
|
||||
mode: Mode,
|
||||
update_event: Ev,
|
||||
}
|
||||
|
||||
impl StyledEditor {
|
||||
pub fn build(id: FieldId) -> StyledEditorBuilder {
|
||||
StyledEditorBuilder {
|
||||
id,
|
||||
initial_text: "".to_string(),
|
||||
text: "".to_string(),
|
||||
html: "".to_string(),
|
||||
mode: Mode::View,
|
||||
update_event: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StyledEditorBuilder {
|
||||
id: FieldId,
|
||||
initial_text: String,
|
||||
text: String,
|
||||
html: String,
|
||||
mode: Mode,
|
||||
update_event: Option<Ev>,
|
||||
}
|
||||
|
||||
impl StyledEditorBuilder {
|
||||
pub fn text<S>(mut self, text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.text = text.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn initial_text<S>(mut self, text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.initial_text = text.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn html<S>(mut self, text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.html = text.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mode(mut self, mode: Mode) -> Self {
|
||||
self.mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledEditor {
|
||||
StyledEditor {
|
||||
id: self.id,
|
||||
initial_text: self.initial_text,
|
||||
text: self.text,
|
||||
html: self.html,
|
||||
mode: self.mode,
|
||||
update_event: self.update_event.unwrap_or(Ev::KeyUp),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_on(mut self, ev: Ev) -> Self {
|
||||
self.update_event = Some(ev);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ToNode for StyledEditor {
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(values: StyledEditor) -> Node<Msg> {
|
||||
let StyledEditor {
|
||||
id,
|
||||
initial_text,
|
||||
text: _,
|
||||
html,
|
||||
mode,
|
||||
update_event,
|
||||
} = values;
|
||||
|
||||
let on_editor_clicked = click_handler(id.clone(), Mode::Editor);
|
||||
let on_view_clicked = click_handler(id.clone(), Mode::View);
|
||||
|
||||
let editor_id = format!("editor-{}", id);
|
||||
let view_id = format!("view-{}", id);
|
||||
let name = format!("styled-editor-{}", id);
|
||||
|
||||
let text_area = StyledTextarea::build(id)
|
||||
.height(40)
|
||||
.update_on(update_event)
|
||||
// .disable_auto_resize()
|
||||
.value(initial_text.as_str())
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let (editor_radio_node, view_radio_node, parsed_node) = match mode {
|
||||
Mode::Editor => (
|
||||
seed::input![
|
||||
id![editor_id.as_str()],
|
||||
attrs![At::Type => "radio"; At::Name => name.as_str(); At::Class => "editorRadio"; At::Checked => true],
|
||||
],
|
||||
seed::input![
|
||||
id![view_id.as_str()],
|
||||
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio";],
|
||||
],
|
||||
vec![],
|
||||
),
|
||||
Mode::View => (
|
||||
seed::input![
|
||||
id![editor_id.as_str()],
|
||||
C!["editorRadio"],
|
||||
attrs![At::Type => "radio"; At::Name => name.as_str();],
|
||||
],
|
||||
seed::input![
|
||||
id![view_id.as_str()],
|
||||
C!["viewRadio"],
|
||||
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
|
||||
],
|
||||
Node::from_html(html.as_str()),
|
||||
),
|
||||
};
|
||||
|
||||
div![
|
||||
C!["styledEditor"],
|
||||
label![
|
||||
if mode == Mode::View {
|
||||
C!["navbar viewTab activeTab"]
|
||||
} else {
|
||||
C!["navbar viewTab"]
|
||||
},
|
||||
attrs![At::For => view_id.as_str()],
|
||||
"View",
|
||||
on_view_clicked
|
||||
],
|
||||
label![
|
||||
if mode == Mode::Editor {
|
||||
C!["navbar editorTab activeTab"]
|
||||
} else {
|
||||
C!["navbar editorTab"]
|
||||
},
|
||||
attrs![At::For => editor_id.as_str()],
|
||||
"Editor",
|
||||
on_editor_clicked
|
||||
],
|
||||
editor_radio_node,
|
||||
text_area,
|
||||
view_radio_node,
|
||||
div![C!["view"], parsed_node],
|
||||
]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn click_handler(field_id: FieldId, new_mode: Mode) -> EventHandler<Msg> {
|
||||
mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::TabChanged(field_id, new_mode))
|
||||
})
|
||||
}
|
||||
|
@ -1,83 +1,79 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::shared::ToNode;
|
||||
use crate::Msg;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyledForm<'l> {
|
||||
heading: &'l str,
|
||||
fields: Vec<Node<Msg>>,
|
||||
on_submit: Option<EventHandler<Msg>>,
|
||||
}
|
||||
|
||||
impl<'l> StyledForm<'l> {
|
||||
pub fn build() -> StyledFormBuilder<'l> {
|
||||
StyledFormBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> ToNode for StyledForm<'l> {
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StyledFormBuilder<'l> {
|
||||
fields: Vec<Node<Msg>>,
|
||||
heading: &'l str,
|
||||
on_submit: Option<EventHandler<Msg>>,
|
||||
}
|
||||
|
||||
impl<'l> StyledFormBuilder<'l> {
|
||||
pub fn add_field(mut self, node: Node<Msg>) -> Self {
|
||||
self.fields.push(node);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn try_field(mut self, node: Option<Node<Msg>>) -> Self {
|
||||
if let Some(n) = node {
|
||||
self.fields.push(n);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn heading(mut self, heading: &'l str) -> Self {
|
||||
self.heading = heading;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_submit(mut self, on_submit: EventHandler<Msg>) -> Self {
|
||||
self.on_submit = Some(on_submit);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledForm<'l> {
|
||||
StyledForm {
|
||||
heading: self.heading,
|
||||
fields: self.fields,
|
||||
on_submit: self.on_submit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(values: StyledForm) -> Node<Msg> {
|
||||
let StyledForm {
|
||||
heading,
|
||||
fields,
|
||||
on_submit,
|
||||
} = values;
|
||||
let handlers = match on_submit {
|
||||
Some(handler) => vec![handler],
|
||||
_ => vec![],
|
||||
};
|
||||
seed::form![
|
||||
handlers,
|
||||
attrs![At::Class => "styledForm"],
|
||||
div![
|
||||
class!["formElement"],
|
||||
div![class!["formHeading"], heading],
|
||||
fields
|
||||
],
|
||||
]
|
||||
}
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::shared::ToNode;
|
||||
use crate::Msg;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyledForm<'l> {
|
||||
heading: &'l str,
|
||||
fields: Vec<Node<Msg>>,
|
||||
on_submit: Option<EventHandler<Msg>>,
|
||||
}
|
||||
|
||||
impl<'l> StyledForm<'l> {
|
||||
pub fn build() -> StyledFormBuilder<'l> {
|
||||
StyledFormBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> ToNode for StyledForm<'l> {
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StyledFormBuilder<'l> {
|
||||
fields: Vec<Node<Msg>>,
|
||||
heading: &'l str,
|
||||
on_submit: Option<EventHandler<Msg>>,
|
||||
}
|
||||
|
||||
impl<'l> StyledFormBuilder<'l> {
|
||||
pub fn add_field(mut self, node: Node<Msg>) -> Self {
|
||||
self.fields.push(node);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn try_field(mut self, node: Option<Node<Msg>>) -> Self {
|
||||
if let Some(n) = node {
|
||||
self.fields.push(n);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn heading(mut self, heading: &'l str) -> Self {
|
||||
self.heading = heading;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_submit(mut self, on_submit: EventHandler<Msg>) -> Self {
|
||||
self.on_submit = Some(on_submit);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledForm<'l> {
|
||||
StyledForm {
|
||||
heading: self.heading,
|
||||
fields: self.fields,
|
||||
on_submit: self.on_submit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(values: StyledForm) -> Node<Msg> {
|
||||
let StyledForm {
|
||||
heading,
|
||||
fields,
|
||||
on_submit,
|
||||
} = values;
|
||||
let handlers = match on_submit {
|
||||
Some(handler) => vec![handler],
|
||||
_ => vec![],
|
||||
};
|
||||
seed::form![
|
||||
handlers,
|
||||
attrs![At::Class => "styledForm"],
|
||||
div![C!["formElement"], div![C!["formHeading"], heading], fields],
|
||||
]
|
||||
}
|
||||
|
@ -346,30 +346,33 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(Option::is_some)
|
||||
.map(|o| o.unwrap())
|
||||
.filter_map(|o| o)
|
||||
.collect();
|
||||
|
||||
let class_list: Vec<seed::Attrs> = class_list
|
||||
.into_iter()
|
||||
.map(|s| match s {
|
||||
Cow::Borrowed(s) => class![s],
|
||||
Cow::Owned(s) => class![s.as_str()],
|
||||
Cow::Borrowed(s) => C![s],
|
||||
Cow::Owned(s) => C![s.as_str()],
|
||||
})
|
||||
.collect();
|
||||
let style_list = style_list
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Cow::Borrowed(s) => s,
|
||||
Cow::Owned(s) => s.as_str(),
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join(";");
|
||||
let style_list = style_list.into_iter().fold("".to_string(), |mut mem, s| {
|
||||
match s {
|
||||
Cow::Borrowed(s) => {
|
||||
mem.push_str(s);
|
||||
}
|
||||
Cow::Owned(owned) => {
|
||||
mem.push_str(owned.as_str());
|
||||
}
|
||||
}
|
||||
mem.push(';');
|
||||
mem
|
||||
});
|
||||
|
||||
i![
|
||||
class!["styledIcon"],
|
||||
C!["styledIcon"],
|
||||
class_list,
|
||||
class![icon.to_str()],
|
||||
C![icon.to_str()],
|
||||
styles,
|
||||
attrs![ At::Style => style_list ],
|
||||
on_click,
|
||||
|
@ -104,19 +104,15 @@ fn render(values: StyledImageInput) -> Node<Msg> {
|
||||
let input_id = id.to_string();
|
||||
|
||||
div![
|
||||
class!["styledImageInput"],
|
||||
C!["styledImageInput"],
|
||||
attrs![At::Class => class_list.join(" ")],
|
||||
label![
|
||||
class!["label"],
|
||||
C!["label"],
|
||||
attrs![At::For => input_id],
|
||||
img![
|
||||
class!["mask"],
|
||||
attrs![At::Src => url.unwrap_or_default()],
|
||||
" "
|
||||
]
|
||||
img![C!["mask"], attrs![At::Src => url.unwrap_or_default()], " "]
|
||||
],
|
||||
input![
|
||||
class!["input"],
|
||||
C!["input"],
|
||||
attrs![At::Type => "file", At::Id => input_id],
|
||||
on_change
|
||||
]
|
||||
|
@ -223,18 +223,14 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
Msg::StrInputChanged(field_id, value)
|
||||
})
|
||||
};
|
||||
let on_keyup = {
|
||||
ev(Ev::KeyUp, move |event| {
|
||||
event.stop_propagation();
|
||||
None as Option<Msg>
|
||||
})
|
||||
};
|
||||
let on_click = {
|
||||
ev(Ev::Click, move |event| {
|
||||
event.stop_propagation();
|
||||
None as Option<Msg>
|
||||
})
|
||||
};
|
||||
let on_keyup = ev(Ev::KeyUp, move |event| {
|
||||
event.stop_propagation();
|
||||
None as Option<Msg>
|
||||
});
|
||||
let on_click = ev(Ev::Click, move |event| {
|
||||
event.stop_propagation();
|
||||
None as Option<Msg>
|
||||
});
|
||||
|
||||
div![
|
||||
C!["styledInput"],
|
||||
@ -251,7 +247,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
At::Id => format!("{}", id),
|
||||
At::Class => input_class_list.join(" "),
|
||||
At::Value => value.unwrap_or_default(),
|
||||
At::Type => input_type.unwrap_or_else(|| "text"),
|
||||
At::Type => input_type.unwrap_or("text"),
|
||||
|
||||
],
|
||||
if auto_focus {
|
||||
|
@ -65,7 +65,7 @@ pub fn render(values: StyledLink) -> Node<Msg> {
|
||||
} = values;
|
||||
|
||||
a![
|
||||
class!["styledLink"],
|
||||
C!["styledLink"],
|
||||
attrs![
|
||||
At::Class => class_list.join(" "),
|
||||
At::Href => href,
|
||||
|
@ -116,9 +116,14 @@ pub fn render(values: StyledModal) -> Node<Msg> {
|
||||
empty![]
|
||||
};
|
||||
|
||||
let close_handler = mouse_ev(Ev::Click, |_| Msg::ModalDropped);
|
||||
let close_handler = mouse_ev(Ev::Click, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::ModalDropped
|
||||
});
|
||||
let body_handler = mouse_ev(Ev::Click, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
None as Option<Msg>
|
||||
});
|
||||
|
||||
|
@ -682,7 +682,7 @@ fn first_row(click_handler: EventHandler<Msg>) -> Node<Msg> {
|
||||
click_handler.clone(),
|
||||
);
|
||||
div![
|
||||
class!["group justify"],
|
||||
C!["group justify"],
|
||||
justify_all_button,
|
||||
justify_center_button,
|
||||
justify_left_button,
|
||||
@ -723,7 +723,7 @@ fn first_row(click_handler: EventHandler<Msg>) -> Node<Msg> {
|
||||
}),
|
||||
);*/
|
||||
div![
|
||||
class!["group system"],
|
||||
C!["group system"],
|
||||
// clip_board_button,
|
||||
// copy_button,
|
||||
// cut_button,
|
||||
@ -777,7 +777,7 @@ fn first_row(click_handler: EventHandler<Msg>) -> Node<Msg> {
|
||||
);
|
||||
|
||||
div![
|
||||
class!["group formatting"],
|
||||
C!["group formatting"],
|
||||
bold_button,
|
||||
italic_button,
|
||||
underline_button,
|
||||
@ -788,7 +788,7 @@ fn first_row(click_handler: EventHandler<Msg>) -> Node<Msg> {
|
||||
]
|
||||
};
|
||||
|
||||
div![class!["row firstRow"], system, formatting, justify]
|
||||
div![C!["row firstRow"], system, formatting, justify]
|
||||
}
|
||||
|
||||
fn second_row(
|
||||
@ -825,7 +825,7 @@ fn second_row(
|
||||
}),
|
||||
);
|
||||
div![
|
||||
class!["group align"],
|
||||
C!["group align"],
|
||||
align_center_button,
|
||||
align_left_button,
|
||||
align_right_button,
|
||||
@ -848,10 +848,10 @@ fn second_row(
|
||||
.empty()
|
||||
.build()
|
||||
.into_node();
|
||||
span![class!["headingOption"], button]
|
||||
span![C!["headingOption"], button]
|
||||
})
|
||||
.collect();
|
||||
let heading_button = span![class!["headingList"], options];
|
||||
let heading_button = span![C!["headingList"], options];
|
||||
|
||||
/*let _field_id = values.field_id.clone();
|
||||
let _small_cap_button = styled_rte_button(
|
||||
@ -872,7 +872,7 @@ fn second_row(
|
||||
}),
|
||||
);*/
|
||||
div![
|
||||
class!["group font"],
|
||||
C!["group font"],
|
||||
// font_button,
|
||||
heading_button,
|
||||
// small_cap_button,
|
||||
@ -924,7 +924,7 @@ fn second_row(
|
||||
code_alt_button.add_child(code_tooltip(values, click_handler.clone()));
|
||||
|
||||
div![
|
||||
class!["group insert"],
|
||||
C!["group insert"],
|
||||
paragraph_button,
|
||||
table_button,
|
||||
code_alt_button,
|
||||
@ -943,11 +943,11 @@ fn second_row(
|
||||
);
|
||||
let outdent_button =
|
||||
styled_rte_button("Outdent", ButtonId::Outdent, Icon::Outdent, click_handler);
|
||||
div![class!["group indentOutdent"], indent_button, outdent_button]
|
||||
div![C!["group indentOutdent"], indent_button, outdent_button]
|
||||
};
|
||||
|
||||
div![
|
||||
class!["row secondRow"],
|
||||
C!["row secondRow"],
|
||||
font_group,
|
||||
// align_group,
|
||||
insert_group,
|
||||
@ -1000,15 +1000,15 @@ fn table_tooltip(
|
||||
let on_submit = click_handler;
|
||||
|
||||
StyledTooltip::build()
|
||||
.table_tooltip()
|
||||
.visible(visible)
|
||||
.add_child(h2![span!["Add table"], close_table_tooltip])
|
||||
.add_child(div![class!["inputs"], span!["Rows"], seed::input![
|
||||
.table_tooltip()
|
||||
.visible(visible)
|
||||
.add_child(h2![span!["Add table"], close_table_tooltip])
|
||||
.add_child(div![C!["inputs"], span!["Rows"], seed::input![
|
||||
attrs![At::Type => "range"; At::Step => "1"; At::Min => "1"; At::Max => "10"; At::Value => rows],
|
||||
on_rows_change
|
||||
]])
|
||||
.add_child(div![
|
||||
class!["inputs"],
|
||||
C!["inputs"],
|
||||
span!["Columns"],
|
||||
seed::input![
|
||||
attrs![At::Type => "range"; At::Step => "1"; At::Min => "1"; At::Max => "10"; At::Value => cols],
|
||||
@ -1025,7 +1025,7 @@ fn table_tooltip(
|
||||
})
|
||||
.collect();
|
||||
seed::div![
|
||||
class!["tablePreview"],
|
||||
C!["tablePreview"],
|
||||
seed::table![tbody![body]],
|
||||
input![attrs![At::Type => "button"; At::Id => "rteInsertTable"; At::Value => "Insert"], on_submit],
|
||||
]
|
||||
@ -1121,9 +1121,5 @@ fn styled_rte_button(
|
||||
.empty()
|
||||
.build()
|
||||
.into_node();
|
||||
span![
|
||||
class!["styledRteButton"],
|
||||
attrs![At::Title => title],
|
||||
button
|
||||
]
|
||||
span![C!["styledRteButton"], attrs![At::Title => title], button]
|
||||
}
|
||||
|
@ -154,15 +154,15 @@ pub fn render(values: StyledSelectChild) -> Node<Msg> {
|
||||
At::Class => name.as_deref().map(|s| format!("{}Label", s)).unwrap_or_default(),
|
||||
At::Class => class_list.join(" "),
|
||||
],
|
||||
class![label_class.as_str()],
|
||||
C![label_class.as_str()],
|
||||
text
|
||||
],
|
||||
_ => empty![],
|
||||
};
|
||||
|
||||
div![
|
||||
class![variant.to_str()],
|
||||
class![wrapper_class.as_str()],
|
||||
C![variant.to_str()],
|
||||
C![wrapper_class.as_str()],
|
||||
attrs![At::Class => class_list.join(" ")],
|
||||
icon_node,
|
||||
label_node
|
||||
|
@ -98,7 +98,7 @@ impl<'l> StyledTextareaBuilder<'l> {
|
||||
height: self.height.unwrap_or(110),
|
||||
class_list: self.class_list,
|
||||
max_height: self.max_height.unwrap_or_default(),
|
||||
update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp),
|
||||
update_event: self.update_event.unwrap_or(Ev::KeyUp),
|
||||
placeholder: self.placeholder,
|
||||
disable_auto_resize: self.disable_auto_resize,
|
||||
}
|
||||
@ -170,36 +170,37 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
|
||||
let text_input_handler = ev(update_event, move |event| {
|
||||
event.stop_propagation();
|
||||
|
||||
let target = event.target().unwrap();
|
||||
let textarea = seed::to_textarea(&target);
|
||||
let value = textarea.value();
|
||||
let value = event
|
||||
.target()
|
||||
.map(|target| seed::to_textarea(&target).value())
|
||||
.unwrap_or_default();
|
||||
|
||||
if handler_disable_auto_resize && value.contains('\n') {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
Msg::StrInputChanged(
|
||||
Some(Msg::StrInputChanged(
|
||||
id,
|
||||
if handler_disable_auto_resize {
|
||||
value.trim().to_string()
|
||||
} else {
|
||||
value
|
||||
},
|
||||
)
|
||||
))
|
||||
});
|
||||
|
||||
class_list.push("textAreaInput");
|
||||
|
||||
div![
|
||||
attrs![At::Class => "styledTextArea"],
|
||||
div![attrs![At::Class => "textAreaHeading"]],
|
||||
C!["styledTextArea"],
|
||||
div![C!["textAreaHeading"]],
|
||||
textarea![
|
||||
attrs![
|
||||
At::Class => class_list.join(" ");
|
||||
At::AutoFocus => "true";
|
||||
At::Style => style_list.join(";");
|
||||
At::Placeholder => placeholder.unwrap_or_default();
|
||||
At::Rows => if disable_auto_resize { "1" } else { "auto" }
|
||||
At::Rows => if disable_auto_resize { "5" } else { "auto" }
|
||||
],
|
||||
value,
|
||||
resize_handler,
|
||||
|
@ -1,20 +1,25 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{TimeTracking, UpdateIssuePayload};
|
||||
|
||||
use crate::modal::time_tracking::value_for_time_tracking;
|
||||
use crate::model::{EditIssueModal, ModalType, Model};
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::ToNode;
|
||||
use crate::Msg;
|
||||
use {
|
||||
crate::{
|
||||
modal::time_tracking::value_for_time_tracking,
|
||||
modals::issues_edit::Model as EditIssueModal,
|
||||
model::{ModalType, Model},
|
||||
shared::{
|
||||
styled_icon::{Icon, StyledIcon},
|
||||
ToNode,
|
||||
},
|
||||
Msg,
|
||||
},
|
||||
jirs_data::{TimeTracking, UpdateIssuePayload},
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
#[inline]
|
||||
pub fn fibonacci_values() -> Vec<u32> {
|
||||
vec![0, 1, 2, 3, 5, 8, 13, 21, 34, 55]
|
||||
}
|
||||
|
||||
pub fn tracking_link(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal { id, .. } = modal;
|
||||
pub fn tracking_link(model: &Model, modal: &crate::modals::issues_edit::Model) -> Node<Msg> {
|
||||
let crate::modals::issues_edit::Model { id, .. } = modal;
|
||||
|
||||
let issue_id = *id;
|
||||
|
||||
@ -22,11 +27,7 @@ pub fn tracking_link(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
Msg::ModalOpened(Box::new(ModalType::TimeTracking(issue_id)))
|
||||
});
|
||||
|
||||
div![
|
||||
class!["trackingLink"],
|
||||
handler,
|
||||
tracking_widget(model, modal),
|
||||
]
|
||||
div![C!["trackingLink"], handler, tracking_widget(model, modal),]
|
||||
}
|
||||
|
||||
pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
@ -71,18 +72,18 @@ pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let remaining_node: Node<Msg> = remaining_node(time_remaining, estimate, time_tracking_type);
|
||||
|
||||
div![
|
||||
class!["trackingWidget"],
|
||||
C!["trackingWidget"],
|
||||
icon,
|
||||
div![
|
||||
class!["right"],
|
||||
C!["right"],
|
||||
div![
|
||||
class!["barCounter"],
|
||||
C!["barCounter"],
|
||||
div![
|
||||
class!["bar"],
|
||||
C!["bar"],
|
||||
attrs![At::Style => format!("width: {}%", bar_width)]
|
||||
]
|
||||
],
|
||||
div![class!["values"], div![spent_text], remaining_node,]
|
||||
div![C!["values"], div![spent_text], remaining_node,]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ pub fn board_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
vec![
|
||||
WsMsg::IssueStatusesLoad,
|
||||
WsMsg::ProjectIssuesLoad,
|
||||
WsMsg::ProjectUsersLoad,
|
||||
WsMsg::EpicsLoad,
|
||||
],
|
||||
model.ws.as_ref(),
|
||||
|
@ -1,164 +1,164 @@
|
||||
use seed::prelude::Orders;
|
||||
use seed::*;
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::model::{Model, PageContent};
|
||||
use crate::ws::send_ws_msg;
|
||||
use crate::Msg;
|
||||
|
||||
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
project_page.issue_drag.drag(issue_id);
|
||||
}
|
||||
|
||||
pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
if project_page.issue_drag.dragged_or_last(issue_bellow_id) {
|
||||
return;
|
||||
}
|
||||
let dragged_id = match project_page.issue_drag.dragged_id.as_ref().cloned() {
|
||||
Some(id) => id,
|
||||
_ => return error!("Nothing is dragged"),
|
||||
};
|
||||
|
||||
let mut below = None;
|
||||
let mut dragged = None;
|
||||
let mut issues = vec![];
|
||||
std::mem::swap(&mut issues, &mut model.issues);
|
||||
|
||||
for issue in issues.into_iter() {
|
||||
match issue.id {
|
||||
id if id == issue_bellow_id => below = Some(issue),
|
||||
id if id == dragged_id => dragged = Some(issue),
|
||||
_ => model.issues.push(issue),
|
||||
};
|
||||
}
|
||||
|
||||
let mut below = match below {
|
||||
Some(below) => below,
|
||||
_ => return,
|
||||
};
|
||||
let mut dragged = match dragged {
|
||||
Some(issue) => issue,
|
||||
_ => {
|
||||
model.issues.push(below);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if dragged.issue_status_id != below.issue_status_id {
|
||||
let mut issues = vec![];
|
||||
std::mem::swap(&mut issues, &mut model.issues);
|
||||
for mut c in issues.into_iter() {
|
||||
if c.issue_status_id == below.issue_status_id && c.list_position > below.list_position {
|
||||
c.list_position += 1;
|
||||
project_page.issue_drag.mark_dirty(c.id);
|
||||
}
|
||||
model.issues.push(c);
|
||||
}
|
||||
dragged.list_position = below.list_position + 1;
|
||||
dragged.issue_status_id = below.issue_status_id;
|
||||
}
|
||||
std::mem::swap(&mut dragged.list_position, &mut below.list_position);
|
||||
|
||||
project_page.issue_drag.mark_dirty(dragged.id);
|
||||
project_page.issue_drag.mark_dirty(below.id);
|
||||
|
||||
model.issues.push(below);
|
||||
model.issues.push(dragged);
|
||||
model
|
||||
.issues
|
||||
.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
project_page.issue_drag.last_id = Some(issue_bellow_id);
|
||||
}
|
||||
|
||||
pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
// log!("------------------------------------------------------------------");
|
||||
// log!("| SYNC |");
|
||||
// log!("------------------------------------------------------------------");
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for issue in model.issues.iter() {
|
||||
if !project_page.issue_drag.dirty.contains(&issue.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
issue.id,
|
||||
IssueFieldId::IssueStatusId,
|
||||
PayloadVariant::I32(issue.issue_status_id),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
issue.id,
|
||||
IssueFieldId::ListPosition,
|
||||
PayloadVariant::I32(issue.list_position),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
project_page.issue_drag.clear();
|
||||
}
|
||||
|
||||
pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let issue_id = match project_page.issue_drag.dragged_id.as_ref().cloned() {
|
||||
Some(issue_id) => issue_id,
|
||||
_ => return error!("Nothing is dragged"),
|
||||
};
|
||||
|
||||
let mut old: Vec<Issue> = vec![];
|
||||
let mut pos = 0;
|
||||
let mut found: Option<Issue> = None;
|
||||
std::mem::swap(&mut old, &mut model.issues);
|
||||
old.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
|
||||
for mut issue in old.into_iter() {
|
||||
if issue.issue_status_id == status_id {
|
||||
if issue.list_position != pos {
|
||||
issue.list_position = pos;
|
||||
project_page.issue_drag.mark_dirty(issue.id);
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
if issue.id != issue_id {
|
||||
model.issues.push(issue);
|
||||
} else {
|
||||
found = Some(issue);
|
||||
}
|
||||
}
|
||||
|
||||
let mut issue = match found {
|
||||
Some(i) => i,
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if issue.issue_status_id == status_id {
|
||||
model.issues.push(issue);
|
||||
} else {
|
||||
issue.issue_status_id = status_id;
|
||||
issue.list_position = pos + 1;
|
||||
model.issues.push(issue);
|
||||
project_page.issue_drag.mark_dirty(issue_id);
|
||||
}
|
||||
}
|
||||
use seed::prelude::Orders;
|
||||
use seed::*;
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::model::{Model, PageContent};
|
||||
use crate::ws::send_ws_msg;
|
||||
use crate::Msg;
|
||||
|
||||
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
project_page.issue_drag.drag(issue_id);
|
||||
}
|
||||
|
||||
pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
if project_page.issue_drag.dragged_or_last(issue_bellow_id) {
|
||||
return;
|
||||
}
|
||||
let dragged_id = match project_page.issue_drag.dragged_id.as_ref().cloned() {
|
||||
Some(id) => id,
|
||||
_ => return error!("Nothing is dragged"),
|
||||
};
|
||||
|
||||
let mut below = None;
|
||||
let mut dragged = None;
|
||||
let mut issues = vec![];
|
||||
std::mem::swap(&mut issues, &mut model.issues);
|
||||
|
||||
for issue in issues.into_iter() {
|
||||
match issue.id {
|
||||
id if id == issue_bellow_id => below = Some(issue),
|
||||
id if id == dragged_id => dragged = Some(issue),
|
||||
_ => model.issues.push(issue),
|
||||
};
|
||||
}
|
||||
|
||||
let mut below = match below {
|
||||
Some(below) => below,
|
||||
_ => return,
|
||||
};
|
||||
let mut dragged = match dragged {
|
||||
Some(issue) => issue,
|
||||
_ => {
|
||||
model.issues.push(below);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if dragged.issue_status_id != below.issue_status_id {
|
||||
let mut issues = vec![];
|
||||
std::mem::swap(&mut issues, &mut model.issues);
|
||||
for mut c in issues.into_iter() {
|
||||
if c.issue_status_id == below.issue_status_id && c.list_position > below.list_position {
|
||||
c.list_position += 1;
|
||||
project_page.issue_drag.mark_dirty(c.id);
|
||||
}
|
||||
model.issues.push(c);
|
||||
}
|
||||
dragged.list_position = below.list_position + 1;
|
||||
dragged.issue_status_id = below.issue_status_id;
|
||||
}
|
||||
std::mem::swap(&mut dragged.list_position, &mut below.list_position);
|
||||
|
||||
project_page.issue_drag.mark_dirty(dragged.id);
|
||||
project_page.issue_drag.mark_dirty(below.id);
|
||||
|
||||
model.issues.push(below);
|
||||
model.issues.push(dragged);
|
||||
model
|
||||
.issues
|
||||
.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
project_page.issue_drag.last_id = Some(issue_bellow_id);
|
||||
}
|
||||
|
||||
pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
// log!("------------------------------------------------------------------");
|
||||
// log!("| SYNC |");
|
||||
// log!("------------------------------------------------------------------");
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for issue in model.issues.iter() {
|
||||
if !project_page.issue_drag.dirty.contains(&issue.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
issue.id,
|
||||
IssueFieldId::IssueStatusId,
|
||||
PayloadVariant::I32(issue.issue_status_id),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
send_ws_msg(
|
||||
WsMsg::IssueUpdate(
|
||||
issue.id,
|
||||
IssueFieldId::ListPosition,
|
||||
PayloadVariant::I32(issue.list_position),
|
||||
),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
}
|
||||
project_page.issue_drag.clear();
|
||||
}
|
||||
|
||||
pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let issue_id = match project_page.issue_drag.dragged_id.as_ref().cloned() {
|
||||
Some(issue_id) => issue_id,
|
||||
_ => return error!("Nothing is dragged"),
|
||||
};
|
||||
|
||||
let mut old: Vec<Issue> = vec![];
|
||||
let mut pos = 0;
|
||||
let mut found: Option<Issue> = None;
|
||||
std::mem::swap(&mut old, &mut model.issues);
|
||||
old.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
|
||||
for mut issue in old.into_iter() {
|
||||
if issue.issue_status_id == status_id {
|
||||
if issue.list_position != pos {
|
||||
issue.list_position = pos;
|
||||
project_page.issue_drag.mark_dirty(issue.id);
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
if issue.id != issue_id {
|
||||
model.issues.push(issue);
|
||||
} else {
|
||||
found = Some(issue);
|
||||
}
|
||||
}
|
||||
|
||||
let mut issue = match found {
|
||||
Some(i) => i,
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if issue.issue_status_id == status_id {
|
||||
model.issues.push(issue);
|
||||
} else {
|
||||
issue.issue_status_id = status_id;
|
||||
issue.list_position = pos + 1;
|
||||
model.issues.push(issue);
|
||||
project_page.issue_drag.mark_dirty(issue_id);
|
||||
}
|
||||
}
|
||||
|
@ -1,324 +1,427 @@
|
||||
use seed::prelude::*;
|
||||
|
||||
pub use init_load_sets::*;
|
||||
use jirs_data::WsMsg;
|
||||
|
||||
use crate::model::*;
|
||||
use crate::shared::{go_to_board, write_auth_token};
|
||||
use crate::{Msg, WebSocketChanged};
|
||||
|
||||
mod init_load_sets;
|
||||
|
||||
pub mod issue;
|
||||
|
||||
pub fn flush_queue(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
use seed::browser::web_socket::State;
|
||||
match model.ws.as_ref() {
|
||||
Some(ws) if ws.state() != State::Open => return,
|
||||
None => return,
|
||||
_ => (),
|
||||
};
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.ws_queue, &mut old);
|
||||
for msg in old {
|
||||
send_ws_msg(msg, model.ws.as_ref(), orders);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enqueue_ws_msg(v: Vec<WsMsg>, ws: Option<&WebSocket>, orders: &mut impl Orders<Msg>) {
|
||||
for msg in v {
|
||||
send_ws_msg(msg, ws, orders);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_ws_msg(msg: WsMsg, ws: Option<&WebSocket>, orders: &mut impl Orders<Msg>) {
|
||||
use seed::browser::web_socket::State;
|
||||
let ws = match ws {
|
||||
Some(ws) if ws.state() == State::Open => ws,
|
||||
_ => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::WebSocketChange(WebSocketChanged::Bounced(msg)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let binary = bincode::serialize(&msg).unwrap();
|
||||
ws.send_bytes(binary.as_slice())
|
||||
.expect("Failed to send ws msg");
|
||||
}
|
||||
|
||||
pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
use seed::browser::web_socket::State;
|
||||
use seed::{prelude::*, *};
|
||||
log!(model.ws.as_ref().map(|ws| ws.state()));
|
||||
|
||||
match model.ws.as_ref() {
|
||||
Some(ws) if ws.state() != State::Closed => {
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
if model.host_url.is_empty() {
|
||||
return;
|
||||
}
|
||||
let url = model.ws_url.as_str();
|
||||
|
||||
model.ws = WebSocket::builder(url, orders)
|
||||
.on_message(|msg| {
|
||||
Some(Msg::WebSocketChange(WebSocketChanged::WebSocketMessage(
|
||||
msg,
|
||||
)))
|
||||
})
|
||||
.on_open(|| {
|
||||
log!("open_socket opened");
|
||||
Some(Msg::WebSocketChange(WebSocketChanged::WebSocketOpened))
|
||||
})
|
||||
.on_close(|_| Some(Msg::WebSocketChange(WebSocketChanged::WebSocketClosed)))
|
||||
.on_error(|| {
|
||||
error!("Failed to open WebSocket");
|
||||
None as Option<Msg>
|
||||
})
|
||||
.protocols(&["jirs"])
|
||||
.build_and_open()
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub async fn read_incoming(msg: WebSocketMessage) -> Msg {
|
||||
let bytes = msg.bytes().await.unwrap_or_default();
|
||||
Msg::WebSocketChange(WebSocketChanged::WebSocketMessageLoaded(bytes))
|
||||
}
|
||||
|
||||
pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
// auth
|
||||
WsMsg::AuthorizeLoaded(Ok(user)) => {
|
||||
model.user = Some(user.clone());
|
||||
if is_non_logged_area() {
|
||||
go_to_board(orders);
|
||||
}
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::UserChanged(model.user.as_ref().cloned()));
|
||||
}
|
||||
WsMsg::AuthorizeExpired => {
|
||||
use seed::*;
|
||||
|
||||
log!("Received token expired");
|
||||
if let Ok(msg) = write_auth_token(None) {
|
||||
orders.skip().send_msg(msg);
|
||||
}
|
||||
}
|
||||
// project
|
||||
WsMsg::ProjectsLoaded(v) => {
|
||||
model.projects = v.clone();
|
||||
init_current_project(model, orders);
|
||||
}
|
||||
// user projects
|
||||
WsMsg::UserProjectsLoaded(v) => {
|
||||
model.user_projects = v.clone();
|
||||
model.current_user_project = v.iter().find(|up| up.is_current).cloned();
|
||||
init_current_project(model, orders);
|
||||
}
|
||||
WsMsg::UserProjectCurrentChanged(user_project) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut model.user_projects);
|
||||
for mut up in old {
|
||||
up.is_current = up.id == user_project.id;
|
||||
model.user_projects.push(up);
|
||||
}
|
||||
model.current_user_project = Some(user_project.clone());
|
||||
init_current_project(model, orders);
|
||||
}
|
||||
|
||||
// issues
|
||||
WsMsg::ProjectIssuesLoaded(v) => {
|
||||
let mut v = v.clone();
|
||||
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
|
||||
model.issues = v;
|
||||
}
|
||||
// issue statuses
|
||||
WsMsg::IssueStatusesLoaded(v) => {
|
||||
model.issue_statuses = v.clone();
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
}
|
||||
WsMsg::IssueStatusCreated(is) => {
|
||||
model.issue_statuses.push(is.clone());
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
}
|
||||
WsMsg::IssueStatusUpdated(changed) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.issue_statuses, &mut old);
|
||||
for is in old {
|
||||
if is.id == changed.id {
|
||||
model.issue_statuses.push(changed.clone());
|
||||
} else {
|
||||
model.issue_statuses.push(is);
|
||||
}
|
||||
}
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
}
|
||||
WsMsg::IssueStatusDeleted(dropped_id, _count) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.issue_statuses, &mut old);
|
||||
for is in old {
|
||||
if is.id != *dropped_id {
|
||||
model.issue_statuses.push(is);
|
||||
}
|
||||
}
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
}
|
||||
WsMsg::IssueDeleted(id, _count) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.issue_statuses, &mut old);
|
||||
for is in old {
|
||||
if is.id == *id {
|
||||
continue;
|
||||
}
|
||||
model.issue_statuses.push(is);
|
||||
}
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
}
|
||||
// users
|
||||
WsMsg::ProjectUsersLoaded(v) => {
|
||||
model.users = v.clone();
|
||||
}
|
||||
// comments
|
||||
WsMsg::IssueCommentsLoaded(comments) => {
|
||||
let issue_id = match model.modals.get(0) {
|
||||
Some(ModalType::EditIssue(issue_id, _)) => *issue_id,
|
||||
_ => return,
|
||||
};
|
||||
if comments.iter().any(|c| c.issue_id != issue_id) {
|
||||
return;
|
||||
}
|
||||
let mut v = comments.clone();
|
||||
v.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
|
||||
model.comments = v;
|
||||
}
|
||||
WsMsg::CommentUpdated(comment) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.comments, &mut old);
|
||||
for current in old.into_iter() {
|
||||
if current.id != comment.id {
|
||||
model.comments.push(current);
|
||||
} else {
|
||||
model.comments.push(comment.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
WsMsg::CommentDeleted(comment_id, _count) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.comments, &mut old);
|
||||
for comment in old.into_iter() {
|
||||
if *comment_id != comment.id {
|
||||
model.comments.push(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
WsMsg::AvatarUrlChanged(user_id, avatar_url) => {
|
||||
for user in model.users.iter_mut() {
|
||||
if user.id == *user_id {
|
||||
user.avatar_url = Some(avatar_url.clone());
|
||||
}
|
||||
}
|
||||
if let Some(me) = model.user.as_mut() {
|
||||
if me.id == *user_id {
|
||||
me.avatar_url = Some(avatar_url.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
// messages
|
||||
WsMsg::Message(received) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut model.messages);
|
||||
for m in old {
|
||||
if m.id != received.id {
|
||||
model.messages.push(m);
|
||||
} else {
|
||||
model.messages.push(received.clone());
|
||||
}
|
||||
}
|
||||
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
}
|
||||
WsMsg::MessagesLoaded(v) => {
|
||||
model.messages = v.clone();
|
||||
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
}
|
||||
WsMsg::MessageMarkedSeen(id, _count) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut model.messages);
|
||||
for m in old {
|
||||
if m.id != *id {
|
||||
model.messages.push(m);
|
||||
}
|
||||
}
|
||||
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
}
|
||||
|
||||
// epics
|
||||
WsMsg::EpicsLoaded(epics) => {
|
||||
model.epics = epics.clone();
|
||||
}
|
||||
WsMsg::EpicCreated(epic) => {
|
||||
model.epics.push(epic.clone());
|
||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
}
|
||||
WsMsg::EpicUpdated(epic) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut model.epics);
|
||||
for current in old {
|
||||
if current.id != epic.id {
|
||||
model.epics.push(current);
|
||||
} else {
|
||||
model.epics.push(epic.clone());
|
||||
}
|
||||
}
|
||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
}
|
||||
WsMsg::EpicDeleted(id, _count) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut model.epics);
|
||||
for current in old {
|
||||
if current.id != *id {
|
||||
model.epics.push(current);
|
||||
}
|
||||
}
|
||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
fn init_current_project(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.projects.is_empty() {
|
||||
return;
|
||||
}
|
||||
model.project = model.current_user_project.as_ref().and_then(|up| {
|
||||
model
|
||||
.projects
|
||||
.iter()
|
||||
.find(|p| p.id == up.project_id)
|
||||
.cloned()
|
||||
});
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::ProjectChanged(model.project.as_ref().cloned()));
|
||||
}
|
||||
|
||||
fn is_non_logged_area() -> bool {
|
||||
let pathname = seed::document().location().unwrap().pathname().unwrap();
|
||||
match pathname.as_str() {
|
||||
"/login" | "/register" | "/invite" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
use seed::prelude::*;
|
||||
|
||||
pub use init_load_sets::*;
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::{
|
||||
model::*,
|
||||
shared::{go_to_board, write_auth_token},
|
||||
Msg, OperationKind, ResourceKind, WebSocketChanged,
|
||||
};
|
||||
|
||||
mod init_load_sets;
|
||||
|
||||
pub mod issue;
|
||||
|
||||
pub fn flush_queue(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
use seed::browser::web_socket::State;
|
||||
match model.ws.as_ref() {
|
||||
Some(ws) if ws.state() != State::Open => return,
|
||||
None => return,
|
||||
_ => (),
|
||||
};
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.ws_queue, &mut old);
|
||||
for msg in old {
|
||||
send_ws_msg(msg, model.ws.as_ref(), orders);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enqueue_ws_msg(v: Vec<WsMsg>, ws: Option<&WebSocket>, orders: &mut impl Orders<Msg>) {
|
||||
for msg in v {
|
||||
send_ws_msg(msg, ws, orders);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_ws_msg(msg: WsMsg, ws: Option<&WebSocket>, orders: &mut impl Orders<Msg>) {
|
||||
use seed::browser::web_socket::State;
|
||||
let ws = match ws {
|
||||
Some(ws) if ws.state() == State::Open => ws,
|
||||
_ => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::WebSocketChange(WebSocketChanged::Bounced(msg)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let binary = bincode::serialize(&msg).unwrap();
|
||||
ws.send_bytes(binary.as_slice())
|
||||
.expect("Failed to send ws msg");
|
||||
}
|
||||
|
||||
pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
use seed::browser::web_socket::State;
|
||||
use seed::{prelude::*, *};
|
||||
log!(model.ws.as_ref().map(|ws| ws.state()));
|
||||
|
||||
match model.ws.as_ref() {
|
||||
Some(ws) if ws.state() != State::Closed => {
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
if model.host_url.is_empty() {
|
||||
return;
|
||||
}
|
||||
let url = model.ws_url.as_str();
|
||||
|
||||
model.ws = WebSocket::builder(url, orders)
|
||||
.on_message(|msg| {
|
||||
Some(Msg::WebSocketChange(WebSocketChanged::WebSocketMessage(
|
||||
msg,
|
||||
)))
|
||||
})
|
||||
.on_open(|| {
|
||||
log!("open_socket opened");
|
||||
Some(Msg::WebSocketChange(WebSocketChanged::WebSocketOpened))
|
||||
})
|
||||
.on_close(|_| Some(Msg::WebSocketChange(WebSocketChanged::WebSocketClosed)))
|
||||
.on_error(|| {
|
||||
error!("Failed to open WebSocket");
|
||||
None as Option<Msg>
|
||||
})
|
||||
.protocols(&["jirs"])
|
||||
.build_and_open()
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub async fn read_incoming(msg: WebSocketMessage) -> Msg {
|
||||
let bytes = msg.bytes().await.unwrap_or_default();
|
||||
Msg::WebSocketChange(WebSocketChanged::WebSocketMessageLoaded(bytes))
|
||||
}
|
||||
|
||||
pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
// auth
|
||||
WsMsg::AuthorizeLoaded(Ok(user)) => {
|
||||
model.user = Some(user);
|
||||
if is_non_logged_area() {
|
||||
go_to_board(orders);
|
||||
}
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::UserChanged(model.user.as_ref().cloned()))
|
||||
.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::User,
|
||||
OperationKind::SingleLoaded,
|
||||
model.user.as_ref().map(|u| u.id),
|
||||
))
|
||||
.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Auth,
|
||||
OperationKind::SingleLoaded,
|
||||
model.user.as_ref().map(|u| u.id),
|
||||
));
|
||||
}
|
||||
WsMsg::AuthorizeExpired => {
|
||||
use seed::*;
|
||||
|
||||
log!("Received token expired");
|
||||
if let Ok(msg) = write_auth_token(None) {
|
||||
orders.skip().send_msg(msg).send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Auth,
|
||||
OperationKind::SingleRemoved,
|
||||
model.user.as_ref().map(|u| u.id),
|
||||
));
|
||||
}
|
||||
}
|
||||
// project
|
||||
WsMsg::ProjectsLoaded(v) => {
|
||||
model.projects = v;
|
||||
init_current_project(model, orders);
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Project,
|
||||
OperationKind::ListLoaded,
|
||||
None,
|
||||
));
|
||||
}
|
||||
// user projects
|
||||
WsMsg::UserProjectsLoaded(v) => {
|
||||
model.current_user_project = v.iter().find(|up| up.is_current).cloned();
|
||||
model.user_projects = v;
|
||||
init_current_project(model, orders);
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::UserProject,
|
||||
OperationKind::ListLoaded,
|
||||
None,
|
||||
));
|
||||
}
|
||||
WsMsg::UserProjectCurrentChanged(user_project) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut model.user_projects);
|
||||
for mut up in old {
|
||||
up.is_current = up.id == user_project.id;
|
||||
model.user_projects.push(up);
|
||||
}
|
||||
model.current_user_project = Some(user_project);
|
||||
init_current_project(model, orders);
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::UserProject,
|
||||
OperationKind::SingleModified,
|
||||
model.current_user_project.as_ref().map(|up| up.id),
|
||||
));
|
||||
}
|
||||
|
||||
// issues
|
||||
WsMsg::ProjectIssuesLoaded(mut v) => {
|
||||
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
|
||||
model.issues = v;
|
||||
model.issues_by_id.clear();
|
||||
for issue in model.issues.iter() {
|
||||
model.issues_by_id.insert(issue.id, issue.clone());
|
||||
}
|
||||
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Issue,
|
||||
OperationKind::ListLoaded,
|
||||
None,
|
||||
));
|
||||
}
|
||||
// issue statuses
|
||||
WsMsg::IssueStatusesLoaded(v) => {
|
||||
model.issue_statuses = v;
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::IssueStatus,
|
||||
OperationKind::ListLoaded,
|
||||
None,
|
||||
));
|
||||
}
|
||||
WsMsg::IssueStatusCreated(is) => {
|
||||
let id = is.id;
|
||||
model.issue_statuses.push(is);
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::IssueStatus,
|
||||
OperationKind::SingleCreated,
|
||||
Some(id),
|
||||
));
|
||||
}
|
||||
WsMsg::IssueStatusUpdated(mut changed) => {
|
||||
let id = changed.id;
|
||||
if let Some(idx) = model.issue_statuses.iter().position(|c| c.id == changed.id) {
|
||||
std::mem::swap(&mut model.issue_statuses[idx], &mut changed);
|
||||
}
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::IssueStatus,
|
||||
OperationKind::SingleModified,
|
||||
Some(id),
|
||||
));
|
||||
}
|
||||
WsMsg::IssueStatusDeleted(dropped_id, _count) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.issue_statuses, &mut old);
|
||||
for is in old {
|
||||
if is.id != dropped_id {
|
||||
model.issue_statuses.push(is);
|
||||
}
|
||||
}
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::IssueStatus,
|
||||
OperationKind::SingleRemoved,
|
||||
Some(dropped_id),
|
||||
));
|
||||
}
|
||||
// issues
|
||||
WsMsg::IssueUpdated(mut issue) => {
|
||||
let id = issue.id;
|
||||
if let Some(idx) = model.issues.iter().position(|i| i.id == issue.id) {
|
||||
std::mem::swap(&mut model.issues[idx], &mut issue);
|
||||
}
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Issue,
|
||||
OperationKind::SingleModified,
|
||||
Some(id),
|
||||
));
|
||||
}
|
||||
WsMsg::IssueDeleted(id, _count) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.issue_statuses, &mut old);
|
||||
for is in old {
|
||||
if is.id == id {
|
||||
continue;
|
||||
}
|
||||
model.issue_statuses.push(is);
|
||||
}
|
||||
model
|
||||
.issue_statuses
|
||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Issue,
|
||||
OperationKind::SingleRemoved,
|
||||
Some(id),
|
||||
));
|
||||
}
|
||||
// users
|
||||
WsMsg::ProjectUsersLoaded(v) => {
|
||||
model.users = v.clone();
|
||||
for user in v {
|
||||
model.users_by_id.insert(user.id, user.clone());
|
||||
}
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::User,
|
||||
OperationKind::ListLoaded,
|
||||
None,
|
||||
));
|
||||
}
|
||||
// comments
|
||||
WsMsg::IssueCommentsLoaded(mut comments) => {
|
||||
let issue_id = match model.modals.get(0) {
|
||||
Some(ModalType::EditIssue(issue_id, _)) => *issue_id,
|
||||
_ => return,
|
||||
};
|
||||
if comments.iter().any(|c| c.issue_id != issue_id) {
|
||||
return;
|
||||
}
|
||||
comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
|
||||
model.comments = comments;
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Comment,
|
||||
OperationKind::ListLoaded,
|
||||
None,
|
||||
));
|
||||
}
|
||||
WsMsg::CommentUpdated(mut comment) => {
|
||||
if let Some(idx) = model.comments.iter().position(|c| c.id == comment.id) {
|
||||
std::mem::swap(&mut model.comments[idx], &mut comment);
|
||||
}
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Comment,
|
||||
OperationKind::SingleModified,
|
||||
Some(comment.id),
|
||||
));
|
||||
}
|
||||
WsMsg::CommentDeleted(comment_id, _count) => {
|
||||
if let Some(idx) = model.comments.iter().position(|c| c.id == comment_id) {
|
||||
model.comments.remove(idx);
|
||||
}
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Comment,
|
||||
OperationKind::SingleRemoved,
|
||||
Some(comment_id),
|
||||
));
|
||||
}
|
||||
WsMsg::AvatarUrlChanged(user_id, avatar_url) => {
|
||||
for user in model.users.iter_mut() {
|
||||
if user.id == user_id {
|
||||
user.avatar_url = Some(avatar_url.clone());
|
||||
}
|
||||
}
|
||||
if let Some(me) = model.user.as_mut() {
|
||||
if me.id == user_id {
|
||||
me.avatar_url = Some(avatar_url.clone());
|
||||
}
|
||||
}
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::User,
|
||||
OperationKind::SingleModified,
|
||||
Some(user_id),
|
||||
));
|
||||
}
|
||||
// messages
|
||||
WsMsg::MessageUpdated(mut received) => {
|
||||
if let Some(idx) = model.messages.iter().position(|m| m.id == received.id) {
|
||||
std::mem::swap(&mut model.messages[idx], &mut received);
|
||||
}
|
||||
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Message,
|
||||
OperationKind::SingleModified,
|
||||
Some(received.id),
|
||||
));
|
||||
}
|
||||
WsMsg::MessagesLoaded(v) => {
|
||||
model.messages = v;
|
||||
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Message,
|
||||
OperationKind::ListLoaded,
|
||||
None,
|
||||
));
|
||||
}
|
||||
WsMsg::MessageMarkedSeen(id, _count) => {
|
||||
if let Some(idx) = model.messages.iter().position(|m| m.id == id) {
|
||||
model.messages.remove(idx);
|
||||
}
|
||||
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Message,
|
||||
OperationKind::SingleRemoved,
|
||||
Some(id),
|
||||
));
|
||||
}
|
||||
|
||||
// epics
|
||||
WsMsg::EpicsLoaded(epics) => {
|
||||
model.epics = epics;
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Epic,
|
||||
OperationKind::ListLoaded,
|
||||
None,
|
||||
));
|
||||
}
|
||||
WsMsg::EpicCreated(epic) => {
|
||||
let id = epic.id;
|
||||
model.epics.push(epic);
|
||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Epic,
|
||||
OperationKind::SingleCreated,
|
||||
Some(id),
|
||||
));
|
||||
}
|
||||
WsMsg::EpicUpdated(mut epic) => {
|
||||
if let Some(idx) = model.epics.iter().position(|e| e.id == epic.id) {
|
||||
std::mem::swap(&mut model.epics[idx], &mut epic);
|
||||
}
|
||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Epic,
|
||||
OperationKind::SingleModified,
|
||||
Some(epic.id),
|
||||
));
|
||||
}
|
||||
WsMsg::EpicDeleted(id, _count) => {
|
||||
if let Some(idx) = model.epics.iter().position(|e| e.id == id) {
|
||||
model.epics.remove(idx);
|
||||
}
|
||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Epic,
|
||||
OperationKind::SingleRemoved,
|
||||
Some(id),
|
||||
));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
fn init_current_project(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.projects.is_empty() {
|
||||
return;
|
||||
}
|
||||
model.project = model.current_user_project.as_ref().and_then(|up| {
|
||||
model
|
||||
.projects
|
||||
.iter()
|
||||
.find(|p| p.id == up.project_id)
|
||||
.cloned()
|
||||
});
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::ProjectChanged(model.project.as_ref().cloned()));
|
||||
}
|
||||
|
||||
fn is_non_logged_area() -> bool {
|
||||
let pathname = seed::document().location().unwrap().pathname().unwrap();
|
||||
matches!(pathname.as_str(), "/login" | "/register" | "/invite")
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user