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-rt",
|
||||||
"actix_derive",
|
"actix_derive",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
@ -34,7 +34,7 @@ dependencies = [
|
|||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix_derive",
|
"actix_derive",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@ -57,7 +57,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
|
checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"log",
|
"log",
|
||||||
@ -72,7 +72,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570"
|
checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"log",
|
"log",
|
||||||
@ -135,14 +135,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.4.1"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d031468a7859f71674e5531bd05137e0ea5de05ec9a917314330b88c582e2e0a"
|
checksum = "c51e8a9146c12fce92a6e4c24b8c4d9b05268130bfd8d61bc587e822c32ce689"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -167,7 +167,7 @@ dependencies = [
|
|||||||
"actix-utils 1.0.6",
|
"actix-utils 1.0.6",
|
||||||
"base64 0.11.0",
|
"base64 0.11.0",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"copyless",
|
"copyless",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
@ -212,8 +212,8 @@ dependencies = [
|
|||||||
"base64 0.13.0",
|
"base64 0.13.0",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"brotli2",
|
"brotli2",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"cookie 0.14.3",
|
"cookie",
|
||||||
"copyless",
|
"copyless",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"either",
|
"either",
|
||||||
@ -263,7 +263,7 @@ dependencies = [
|
|||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-utils 2.0.0",
|
"actix-utils 2.0.0",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"httparse",
|
"httparse",
|
||||||
@ -381,7 +381,7 @@ dependencies = [
|
|||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"either",
|
"either",
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
"log",
|
"log",
|
||||||
@ -399,7 +399,7 @@ dependencies = [
|
|||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"either",
|
"either",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@ -428,7 +428,7 @@ dependencies = [
|
|||||||
"actix-utils 2.0.0",
|
"actix-utils 2.0.0",
|
||||||
"actix-web-codegen",
|
"actix-web-codegen",
|
||||||
"awc",
|
"awc",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@ -458,7 +458,7 @@ dependencies = [
|
|||||||
"actix-codec 0.3.0",
|
"actix-codec 0.3.0",
|
||||||
"actix-http 2.2.0",
|
"actix-http 2.2.0",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"pin-project 0.4.27",
|
"pin-project 0.4.27",
|
||||||
@ -488,9 +488,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.14.0"
|
version = "0.14.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
|
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gimli",
|
"gimli",
|
||||||
]
|
]
|
||||||
@ -510,6 +510,30 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "ansi_term"
|
name = "ansi_term"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -582,7 +606,7 @@ dependencies = [
|
|||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"base64 0.13.0",
|
"base64 0.13.0",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -742,11 +766,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "buf-min"
|
name = "buf-min"
|
||||||
version = "0.2.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "881e704e61d0fb41d7c6c9ae2bd790eb8c13dc974ae102fb98c788b4fdea4349"
|
checksum = "fa17aa1cf56bdd6bb30518767d00e58019d326f3f05d8c3e0730b549d332ea83"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.6.0",
|
"bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -779,19 +803,13 @@ version = "0.5.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
|
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytes"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytestring"
|
name = "bytestring"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363"
|
checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -899,16 +917,6 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
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]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
@ -1146,9 +1154,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "0.4.6"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b"
|
checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
@ -1324,7 +1332,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"actix 0.10.0",
|
"actix 0.10.0",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
"jirs-config",
|
"jirs-config",
|
||||||
@ -1653,7 +1661,7 @@ version = "0.2.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535"
|
checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@ -1752,7 +1760,7 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26"
|
checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
"itoa",
|
"itoa",
|
||||||
]
|
]
|
||||||
@ -1763,7 +1771,7 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b"
|
checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1794,7 +1802,7 @@ version = "0.13.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf"
|
checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -1818,7 +1826,7 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed"
|
checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"hyper",
|
"hyper",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1916,9 +1924,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.6"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
|
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jirs-cli"
|
name = "jirs-cli"
|
||||||
@ -1973,6 +1981,7 @@ dependencies = [
|
|||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
|
"amazon-actor",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -2014,7 +2023,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"comrak",
|
|
||||||
"futures 0.1.30",
|
"futures 0.1.30",
|
||||||
"jirs-data",
|
"jirs-data",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@ -2320,9 +2328,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f"
|
checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
@ -2469,9 +2477,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.31"
|
version = "0.10.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d008f51b1acffa0d3450a68606e6a51c123012edaacb0f4e1426bd978869187"
|
checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
@ -2498,9 +2506,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.59"
|
version = "0.9.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de52d8eabd217311538a39bba130d7dea1f1e118010fee7a033d966845e7d5fe"
|
checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg 1.0.1",
|
"autocfg 1.0.1",
|
||||||
"cc",
|
"cc",
|
||||||
@ -2734,9 +2742,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.7.2"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca36dea94d187597e104a5c8e4b07576a8a45aa5db48a65e12940d3eb7461f55"
|
checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"getopts",
|
"getopts",
|
||||||
@ -3030,7 +3038,7 @@ checksum = "e977941ee0658df96fca7291ecc6fc9a754600b21ad84b959eb1dbbc9d5abcc7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.12.3",
|
"base64 0.12.3",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
"http",
|
"http",
|
||||||
@ -3077,7 +3085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1146e37a7c1df56471ea67825fe09bbbd37984b5f6e201d8b2e0be4ee15643d8"
|
checksum = "1146e37a7c1df56471ea67825fe09bbbd37984b5f6e201d8b2e0be4ee15643d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
"rusoto_core",
|
"rusoto_core",
|
||||||
"xml-rs",
|
"xml-rs",
|
||||||
@ -3090,7 +3098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "97a740a88dde8ded81b6f2cff9cd5e054a5a2e38a38397260f7acdd2c85d17dd"
|
checksum = "97a740a88dde8ded81b6f2cff9cd5e054a5a2e38a38397260f7acdd2c85d17dd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.12.3",
|
"base64 0.12.3",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
@ -3212,12 +3220,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "seed"
|
name = "seed"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "882f4569a394bbb2f15f2fc410e0fbcef178fe24fc2d91599607a598443c6df8"
|
checksum = "3b599be9cc57456f4b7fc99b8abfb154d4819f7b6c147e80be5580663dad4536"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"cookie 0.13.3",
|
"cookie",
|
||||||
"dbg",
|
"dbg",
|
||||||
"enclose",
|
"enclose",
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
@ -3273,9 +3281,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.60"
|
version = "1.0.61"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779"
|
checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@ -3395,10 +3403,16 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "standback"
|
name = "spin"
|
||||||
version = "0.2.13"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"version_check 0.9.2",
|
"version_check 0.9.2",
|
||||||
]
|
]
|
||||||
@ -3466,9 +3480,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.55"
|
version = "1.0.56"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a"
|
checksum = "a9802ddde94170d186eeee5005b798d9c159fa970403f1be19976d0cfb939b72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3555,18 +3569,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e"
|
checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56"
|
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3661,7 +3675,7 @@ version = "0.2.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48"
|
checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"iovec",
|
"iovec",
|
||||||
@ -3705,7 +3719,7 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930"
|
checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"log",
|
"log",
|
||||||
@ -3719,7 +3733,7 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
|
checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@ -4002,9 +4016,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "v_escape"
|
name = "v_escape"
|
||||||
version = "0.14.1"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccca9e73c678b882900cbaec16dae4d3662ace5a17774ac45af04e0f3988fafa"
|
checksum = "f3e0ab5fab1db278a9413d2ea794cb66f471f898c5b020c3c394f6447625d9d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"buf-min",
|
"buf-min",
|
||||||
"v_escape_derive",
|
"v_escape_derive",
|
||||||
@ -4024,9 +4038,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "v_htmlescape"
|
name = "v_htmlescape"
|
||||||
version = "0.11.0"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db00c903248abee8499af60bf20d242e7882335bbbffd2614915184cbb207402"
|
checksum = "1f9a8af610ad6f7fc9989c9d2590d9764bc61f294884e9ee93baa58795174572"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"v_escape",
|
"v_escape",
|
||||||
@ -4192,26 +4206,21 @@ dependencies = [
|
|||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
|
"amazon-actor",
|
||||||
"bincode",
|
"bincode",
|
||||||
"bytes 0.5.6",
|
"bytes",
|
||||||
"database-actor",
|
"database-actor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"filesystem-actor",
|
"filesystem-actor",
|
||||||
"flate2",
|
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
"jirs-config",
|
"jirs-config",
|
||||||
"jirs-data",
|
"jirs-data",
|
||||||
"lazy_static",
|
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"mail-actor",
|
"mail-actor",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"rusoto_core",
|
|
||||||
"rusoto_s3",
|
|
||||||
"rusoto_signature",
|
|
||||||
"serde",
|
"serde",
|
||||||
"syntect",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"uuid 0.8.1",
|
"uuid 0.8.1",
|
||||||
@ -4236,10 +4245,12 @@ dependencies = [
|
|||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"comrak",
|
||||||
"database-actor",
|
"database-actor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures 0.3.8",
|
"futures 0.3.8",
|
||||||
|
"highlight-actor",
|
||||||
"jirs-config",
|
"jirs-config",
|
||||||
"jirs-data",
|
"jirs-data",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@ -4248,6 +4259,7 @@ dependencies = [
|
|||||||
"mail-actor",
|
"mail-actor",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
|
"pulldown-cmark",
|
||||||
"serde",
|
"serde",
|
||||||
"syntect",
|
"syntect",
|
||||||
"toml",
|
"toml",
|
||||||
@ -4263,6 +4275,7 @@ dependencies = [
|
|||||||
"cfg-if 0.1.10",
|
"cfg-if 0.1.10",
|
||||||
"libc",
|
"libc",
|
||||||
"memory_units",
|
"memory_units",
|
||||||
|
"spin",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
members = [
|
members = [
|
||||||
"./jirs-cli",
|
"./jirs-cli",
|
||||||
"./jirs-server",
|
"./jirs-server",
|
||||||
"./jirs-client",
|
|
||||||
"./jirs-css",
|
"./jirs-css",
|
||||||
"./shared/jirs-config",
|
"./shared/jirs-config",
|
||||||
"./shared/jirs-data",
|
"./shared/jirs-data",
|
||||||
@ -22,5 +21,8 @@ members = [
|
|||||||
"./actors/web-actor",
|
"./actors/web-actor",
|
||||||
"./actors/websocket-actor",
|
"./actors/websocket-actor",
|
||||||
"./actors/mail-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 personal settings to choose MDE (Markdown Editor) or RTE
|
||||||
* Add issues and filters
|
* 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
|
##### Work Progress
|
||||||
|
|
||||||
* [X] Add Epic
|
* [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 {
|
use {
|
||||||
actix::{Actor, Handler, SyncContext},
|
actix::{Actor, Handler, SyncContext},
|
||||||
std::sync::Arc,
|
std::sync::Arc,
|
||||||
@ -45,17 +46,74 @@ impl Actor for HighlightActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(actix::Message)]
|
#[derive(actix::Message)]
|
||||||
#[rtype(result = "Result<Vec<u8>, HighlightError>")]
|
#[rtype(result = "Result<HighlightedCode, HighlightError>")]
|
||||||
pub struct HighlightCode {
|
pub struct HighlightCode {
|
||||||
pub code: String,
|
pub code: String,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler<HighlightCode> for HighlightActor {
|
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 {
|
fn handle(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let res = hi(&msg.code, &msg.lang)?;
|
let res: Vec<(Style, &str)> = hi(&msg.code, &msg.lang)?;
|
||||||
bincode::serialize(&res).map_err(|_| HighlightError::ResultUnserializable)
|
|
||||||
|
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]
|
[features]
|
||||||
local-storage = ["filesystem-actor"]
|
local-storage = ["filesystem-actor"]
|
||||||
aws-s3 = ["rusoto_s3", "rusoto_core"]
|
aws-s3 = ["amazon-actor"]
|
||||||
default = ["local-storage", "aws-s3"]
|
default = ["local-storage", "aws-s3"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@ -36,10 +36,6 @@ futures = { version = "0.3.8" }
|
|||||||
openssl-sys = { version = "*", features = ["vendored"] }
|
openssl-sys = { version = "*", features = ["vendored"] }
|
||||||
libc = { version = "0.2.0", default-features = false }
|
libc = { version = "0.2.0", default-features = false }
|
||||||
|
|
||||||
flate2 = { version = "*" }
|
|
||||||
syntect = { version = "*" }
|
|
||||||
lazy_static = { version = "*" }
|
|
||||||
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
@ -67,18 +63,9 @@ path = "../websocket-actor"
|
|||||||
path = "../filesystem-actor"
|
path = "../filesystem-actor"
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
# Amazon S3
|
[dependencies.amazon-actor]
|
||||||
[dependencies.rusoto_s3]
|
path = "../amazon-actor"
|
||||||
optional = true
|
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]
|
[dependencies.tokio]
|
||||||
version = "0.2.23"
|
version = "0.2.23"
|
||||||
|
@ -21,6 +21,7 @@ pub async fn upload(
|
|||||||
db: Data<Addr<DbExecutor>>,
|
db: Data<Addr<DbExecutor>>,
|
||||||
ws: Data<Addr<WsServer>>,
|
ws: Data<Addr<WsServer>>,
|
||||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||||
|
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let mut user_id: Option<UserId> = None;
|
let mut user_id: Option<UserId> = None;
|
||||||
let mut avatar_url: Option<String> = None;
|
let mut avatar_url: Option<String> = None;
|
||||||
@ -45,6 +46,7 @@ pub async fn upload(
|
|||||||
field,
|
field,
|
||||||
disposition,
|
disposition,
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
|
amazon.clone(),
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
|
@ -1,51 +1,40 @@
|
|||||||
#[cfg(feature = "local-storage")]
|
|
||||||
use filesystem_actor::FileSystemExecutor;
|
|
||||||
use {
|
use {
|
||||||
actix::Addr,
|
actix::Addr,
|
||||||
actix_multipart::Field,
|
actix_multipart::Field,
|
||||||
actix_web::{http::header::ContentDisposition, web::Data, Error},
|
actix_web::{http::header::ContentDisposition, web::Data, Error},
|
||||||
futures::{StreamExt, TryStreamExt},
|
futures::StreamExt,
|
||||||
jirs_data::UserId,
|
jirs_data::UserId,
|
||||||
rusoto_core::ByteStream,
|
|
||||||
tokio::sync::broadcast::{Receiver, Sender},
|
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"))]
|
#[cfg(all(feature = "local-storage", feature = "aws-s3"))]
|
||||||
pub(crate) async fn handle_image(
|
pub(crate) async fn handle_image(
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
mut field: Field,
|
mut field: Field,
|
||||||
disposition: ContentDisposition,
|
disposition: ContentDisposition,
|
||||||
fs: Data<Addr<FileSystemExecutor>>,
|
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||||
|
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let filename = disposition.get_filename().unwrap();
|
let filename = disposition.get_filename().unwrap();
|
||||||
let system_file_name = format!("{}-{}", user_id, filename);
|
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(
|
let fs_fut = local_storage_write(system_file_name.clone(), fs, user_id, sender.subscribe());
|
||||||
system_file_name.clone(),
|
let aws_fut = aws_s3(system_file_name, amazon, receiver);
|
||||||
fs.clone(),
|
let read_fut = read_form_data(&mut field, sender);
|
||||||
user_id,
|
|
||||||
sender.subscribe(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Upload to AWS S3
|
let fs_join = tokio::task::spawn(fs_fut);
|
||||||
let aws_fut = tokio::task::spawn(aws_s3(system_file_name, receiver));
|
let aws_join = tokio::task::spawn(aws_fut);
|
||||||
|
read_fut.await;
|
||||||
read_form_data(&mut field, sender).await;
|
|
||||||
|
|
||||||
let mut new_link = None;
|
let mut new_link = None;
|
||||||
|
|
||||||
if let Ok(url) = fs_fut.await {
|
if let Ok(url) = fs_join.await {
|
||||||
new_link = url;
|
new_link = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(url) = aws_fut.await {
|
if let Ok(url) = aws_join.await {
|
||||||
new_link = url;
|
new_link = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,31 +46,25 @@ pub(crate) async fn handle_image(
|
|||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
mut field: Field,
|
mut field: Field,
|
||||||
disposition: ContentDisposition,
|
disposition: ContentDisposition,
|
||||||
fs: Data<Addr<FileSystemExecutor>>,
|
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let filename = disposition.get_filename().unwrap();
|
let filename = disposition.get_filename().unwrap();
|
||||||
let system_file_name = format!("{}-{}", user_id, filename);
|
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, amazon, receiver);
|
||||||
let aws_fut = aws_s3(system_file_name, 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! {
|
let mut new_link = None;
|
||||||
b = aws_fut => b,
|
|
||||||
};
|
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())
|
Ok(new_link.unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,35 +73,25 @@ pub(crate) async fn handle_image(
|
|||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
mut field: Field,
|
mut field: Field,
|
||||||
disposition: ContentDisposition,
|
disposition: ContentDisposition,
|
||||||
fs: Data<Addr<FileSystemExecutor>>,
|
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let filename = disposition.get_filename().unwrap();
|
let filename = disposition.get_filename().unwrap();
|
||||||
let system_file_name = format!("{}-{}", user_id, filename);
|
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(
|
let fs_fut = local_storage_write(system_file_name, fs, user_id, sender.subscribe());
|
||||||
system_file_name.clone(),
|
let read_fut = read_form_data(&mut field, sender);
|
||||||
fs.clone(),
|
|
||||||
user_id,
|
|
||||||
sender.subscribe(),
|
|
||||||
);
|
|
||||||
|
|
||||||
read_form_data(&mut field, sender).await;
|
let fs_join = tokio::task::spawn(fs_fut);
|
||||||
|
read_fut.await;
|
||||||
|
|
||||||
let new_link = tokio::select! {
|
let mut new_link = None;
|
||||||
a = fs_fut => a,
|
|
||||||
};
|
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())
|
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
|
/// Stream bytes directly to AWS S3 Service
|
||||||
#[cfg(feature = "aws-s3")]
|
#[cfg(feature = "aws-s3")]
|
||||||
async fn aws_s3(system_file_name: String, mut receiver: Receiver<bytes::Bytes>) -> Option<String> {
|
async fn aws_s3(
|
||||||
let web_config = jirs_config::web::Configuration::read();
|
system_file_name: String,
|
||||||
let s3 = &web_config.s3;
|
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||||
|
receiver: Receiver<bytes::Bytes>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let s3 = jirs_config::amazon::config();
|
||||||
if !s3.active {
|
if !s3.active {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
s3.set_variables();
|
|
||||||
log::debug!("{:?}", s3);
|
|
||||||
|
|
||||||
let mut v: Vec<u8> = vec![];
|
match amazon
|
||||||
use bytes::Buf;
|
.send(amazon_actor::S3PutObject {
|
||||||
|
source: receiver,
|
||||||
while let Ok(b) = receiver.recv().await {
|
file_name: system_file_name.to_string(),
|
||||||
v.extend_from_slice(b.bytes())
|
})
|
||||||
|
.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")]
|
#[cfg(feature = "local-storage")]
|
||||||
async fn local_storage_write(
|
async fn local_storage_write(
|
||||||
system_file_name: String,
|
system_file_name: String,
|
||||||
@ -179,17 +136,17 @@ async fn local_storage_write(
|
|||||||
user_id: jirs_data::UserId,
|
user_id: jirs_data::UserId,
|
||||||
receiver: Receiver<bytes::Bytes>,
|
receiver: Receiver<bytes::Bytes>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let web_config = jirs_config::web::Configuration::read();
|
let web_config = jirs_config::web::config();
|
||||||
let fs_config = jirs_config::fs::Configuration::read();
|
let fs_config = jirs_config::fs::config();
|
||||||
|
|
||||||
let _ = fs
|
match fs
|
||||||
.send(filesystem_actor::CreateFile {
|
.send(filesystem_actor::CreateFile {
|
||||||
source: receiver,
|
source: receiver,
|
||||||
file_name: system_file_name.clone(),
|
file_name: system_file_name.to_string(),
|
||||||
})
|
})
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
Some(format!(
|
Ok(Ok(_)) => Some(format!(
|
||||||
"{proto}://{bind}{port}{client_path}/{user_id}-{filename}",
|
"{proto}://{bind}{port}{client_path}/{user_id}-{filename}",
|
||||||
proto = if web_config.ssl { "https" } else { "http" },
|
proto = if web_config.ssl { "https" } else { "http" },
|
||||||
bind = web_config.bind,
|
bind = web_config.bind,
|
||||||
@ -200,15 +157,8 @@ async fn local_storage_write(
|
|||||||
client_path = fs_config.client_path,
|
client_path = fs_config.client_path,
|
||||||
user_id = user_id,
|
user_id = user_id,
|
||||||
filename = system_file_name
|
filename = system_file_name
|
||||||
))
|
)),
|
||||||
}
|
Ok(_) => None,
|
||||||
|
_ => None,
|
||||||
#[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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,12 @@ env_logger = "0.7"
|
|||||||
|
|
||||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||||
|
|
||||||
|
[dependencies.comrak]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
|
[dependencies.pulldown-cmark]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
[dependencies.jirs-config]
|
[dependencies.jirs-config]
|
||||||
path = "../../shared/jirs-config"
|
path = "../../shared/jirs-config"
|
||||||
features = ["websocket"]
|
features = ["websocket"]
|
||||||
@ -48,3 +54,6 @@ path = "../database-actor"
|
|||||||
|
|
||||||
[dependencies.mail-actor]
|
[dependencies.mail-actor]
|
||||||
path = "../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(
|
self.addr.do_send(InnerMsg::SendToUser(
|
||||||
message.receiver_id,
|
message.receiver_id,
|
||||||
WsMsg::Message(message),
|
WsMsg::MessageUpdated(message),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,64 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
|
|||||||
msg.title = Some(s);
|
msg.title = Some(s);
|
||||||
}
|
}
|
||||||
(IssueFieldId::Description, PayloadVariant::String(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)) => {
|
(IssueFieldId::IssueStatusId, PayloadVariant::I32(s)) => {
|
||||||
msg.issue_status_id = Some(s);
|
msg.issue_status_id = Some(s);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
pub use {
|
pub use {
|
||||||
auth::*, comments::*, epics::*, invitations::*, issue_statuses::*, issues::*, messages::*,
|
auth::*, comments::*, epics::*, hi::*, invitations::*, issue_statuses::*, issues::*,
|
||||||
projects::*, user_projects::*, users::*,
|
messages::*, projects::*, user_projects::*, users::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod epics;
|
pub mod epics;
|
||||||
|
pub mod hi;
|
||||||
pub mod invitations;
|
pub mod invitations;
|
||||||
pub mod issue_statuses;
|
pub mod issue_statuses;
|
||||||
pub mod issues;
|
pub mod issues;
|
||||||
|
@ -34,6 +34,7 @@ struct WebSocketActor {
|
|||||||
db: Data<Addr<DbExecutor>>,
|
db: Data<Addr<DbExecutor>>,
|
||||||
mail: Data<Addr<MailExecutor>>,
|
mail: Data<Addr<MailExecutor>>,
|
||||||
addr: Addr<WsServer>,
|
addr: Addr<WsServer>,
|
||||||
|
hi: Data<Addr<highlight_actor::HighlightActor>>,
|
||||||
current_user: Option<jirs_data::User>,
|
current_user: Option<jirs_data::User>,
|
||||||
current_user_project: Option<jirs_data::UserProject>,
|
current_user_project: Option<jirs_data::UserProject>,
|
||||||
current_project: Option<jirs_data::Project>,
|
current_project: Option<jirs_data::Project>,
|
||||||
@ -186,6 +187,9 @@ impl WebSocketActor {
|
|||||||
self.handle_msg(epics::UpdateEpic { epic_id, name }, ctx)?
|
self.handle_msg(epics::UpdateEpic { epic_id, name }, ctx)?
|
||||||
}
|
}
|
||||||
WsMsg::EpicDelete(epic_id) => self.handle_msg(epics::DeleteEpic { epic_id }, 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
|
// else fail
|
||||||
_ => {
|
_ => {
|
||||||
@ -323,11 +327,13 @@ pub async fn index(
|
|||||||
db: Data<Addr<DbExecutor>>,
|
db: Data<Addr<DbExecutor>>,
|
||||||
mail: Data<Addr<MailExecutor>>,
|
mail: Data<Addr<MailExecutor>>,
|
||||||
ws_server: Data<Addr<WsServer>>,
|
ws_server: Data<Addr<WsServer>>,
|
||||||
|
hi: Data<Addr<highlight_actor::HighlightActor>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
ws::start(
|
ws::start(
|
||||||
WebSocketActor {
|
WebSocketActor {
|
||||||
db,
|
db,
|
||||||
mail,
|
mail,
|
||||||
|
hi,
|
||||||
current_user: None,
|
current_user: None,
|
||||||
current_user_project: None,
|
current_user_project: None,
|
||||||
current_project: None,
|
current_project: None,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# see diesel.rs/guides/configuring-diesel-cli
|
# see diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
[print_schema]
|
[print_schema]
|
||||||
file = "database-actor/src/schema.rs"
|
file = "actors/database-actor/src/schema.rs"
|
||||||
import_types = ["diesel::sql_types::*", "jirs_data::sql::*"]
|
import_types = ["diesel::sql_types::*", "jirs_data::sql::*"]
|
||||||
with_docs = true
|
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"
|
name = "jirs_client"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
print-model = []
|
||||||
|
default = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jirs-data = { path = "../shared/jirs-data", features = ["frontend"] }
|
jirs-data = { path = "../shared/jirs-data", features = ["frontend"] }
|
||||||
|
|
||||||
wee_alloc = "*"
|
seed = { version = "0.8.0" }
|
||||||
|
|
||||||
seed = { version = "0.7.0" }
|
|
||||||
|
|
||||||
serde = { version = "*" }
|
serde = { version = "*" }
|
||||||
serde_json = { version = "*" }
|
serde_json = { version = "*" }
|
||||||
@ -27,7 +29,10 @@ bincode = { version = "*" }
|
|||||||
chrono = { version = "0.4", default-features = false, features = ["serde", "wasmbind"] }
|
chrono = { version = "0.4", default-features = false, features = ["serde", "wasmbind"] }
|
||||||
uuid = { version = "0.8.1", features = ["serde"] }
|
uuid = { version = "0.8.1", features = ["serde"] }
|
||||||
futures = "^0.1.26"
|
futures = "^0.1.26"
|
||||||
comrak = "*"
|
|
||||||
|
[dependencies.wee_alloc]
|
||||||
|
version = "*"
|
||||||
|
features = ["static_array_backend"]
|
||||||
|
|
||||||
[dependencies.wasm-bindgen]
|
[dependencies.wasm-bindgen]
|
||||||
version = "*"
|
version = "*"
|
||||||
|
@ -84,9 +84,13 @@
|
|||||||
color: var(--textMedium);
|
color: var(--textMedium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#projectPage > .rows > .row > .epicName {
|
||||||
|
margin: 18px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
#projectPage > .rows > .row > .projectBoardLists {
|
#projectPage > .rows > .row > .projectBoardLists {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 26px -5px 0;
|
margin: 10px -5px 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@ -276,8 +276,8 @@ impl ElementBuilder {
|
|||||||
pub fn mount(&self) {
|
pub fn mount(&self) {
|
||||||
let source = self.to_js();
|
let source = self.to_js();
|
||||||
{
|
{
|
||||||
use seed::*;
|
// use seed::*;
|
||||||
log!(source);
|
// log!(source);
|
||||||
}
|
}
|
||||||
use seed::*;
|
use seed::*;
|
||||||
match js_sys::eval(source.as_str()) {
|
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)]
|
#![feature(or_patterns, type_ascription)]
|
||||||
|
|
||||||
use seed::{prelude::*, *};
|
use {
|
||||||
use web_sys::File;
|
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_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;
|
mod changes;
|
||||||
pub mod elements;
|
pub mod elements;
|
||||||
mod fields;
|
mod fields;
|
||||||
mod invite;
|
mod images;
|
||||||
mod modal;
|
mod modal;
|
||||||
|
mod modals;
|
||||||
mod model;
|
mod model;
|
||||||
mod profile;
|
mod pages;
|
||||||
mod project;
|
|
||||||
mod project_settings;
|
|
||||||
mod reports;
|
|
||||||
mod shared;
|
mod shared;
|
||||||
mod sign_in;
|
|
||||||
mod sign_up;
|
|
||||||
mod users;
|
|
||||||
pub mod validations;
|
pub mod validations;
|
||||||
mod ws;
|
mod ws;
|
||||||
|
|
||||||
#[global_allocator]
|
// #[global_allocator]
|
||||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
// 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)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
@ -112,6 +134,9 @@ pub enum Msg {
|
|||||||
|
|
||||||
// WebSocket
|
// WebSocket
|
||||||
WebSocketChange(WebSocketChanged),
|
WebSocketChange(WebSocketChanged),
|
||||||
|
|
||||||
|
// resource changes
|
||||||
|
ResourceChanged(ResourceKind, OperationKind, Option<i32>),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
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();
|
orders.skip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WebSocketChanged::WsMsg(ref ws_msg) => {
|
WebSocketChanged::WsMsg(ws_msg) => {
|
||||||
ws::update(ws_msg, model, orders);
|
ws::update(ws_msg, model, orders);
|
||||||
|
orders.skip();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
WebSocketChanged::WebSocketMessageLoaded(v) => {
|
WebSocketChanged::WebSocketMessageLoaded(v) => {
|
||||||
match bincode::deserialize(v.as_slice()) {
|
match bincode::deserialize(v.as_slice()) {
|
||||||
@ -187,7 +214,6 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Msg::ChangePage(page) => {
|
Msg::ChangePage(page) => {
|
||||||
orders.skip();
|
|
||||||
model.page = *page;
|
model.page = *page;
|
||||||
}
|
}
|
||||||
Msg::ToggleTooltip(variant) => match variant {
|
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);
|
use crate::shared::{aside, navbar_left};
|
||||||
match model.page {
|
aside::update(&msg, model, orders);
|
||||||
Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders),
|
navbar_left::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),
|
|
||||||
}
|
}
|
||||||
if cfg!(debug_assertions) {
|
crate::modal::update(&msg, model, orders);
|
||||||
// debug!(model);
|
|
||||||
|
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> {
|
fn view(model: &model::Model) -> Node<Msg> {
|
||||||
match model.page {
|
match model.page {
|
||||||
Page::Project | Page::AddIssue => project::view(model),
|
Page::Project | Page::AddIssue => pages::project_page::view(model),
|
||||||
Page::EditIssue(_id) => project::view(model),
|
Page::EditIssue(_id) => pages::project_page::view(model),
|
||||||
Page::ProjectSettings => project_settings::view(model),
|
Page::ProjectSettings => pages::project_settings_page::view(model),
|
||||||
Page::SignIn => sign_in::view(model),
|
Page::SignIn => pages::sign_in_page::view(model),
|
||||||
Page::SignUp => sign_up::view(model),
|
Page::SignUp => pages::sign_up_page::view(model),
|
||||||
Page::Invite => invite::view(model),
|
Page::Invite => pages::invite_page::view(model),
|
||||||
Page::Users => users::view(model),
|
Page::Users => pages::users_page::view(model),
|
||||||
Page::Profile => profile::view(model),
|
Page::Profile => pages::profile_page::view(model),
|
||||||
Page::Reports => reports::view(model),
|
Page::Reports => pages::reports_page::view(model),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn routes(url: Url) -> Option<Msg> {
|
|
||||||
match resolve_page(url) {
|
|
||||||
Some(page) => Some(Msg::ChangePage(page)),
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,14 +303,39 @@ pub fn render(host_url: String, ws_url: String) {
|
|||||||
}
|
}
|
||||||
elements::define();
|
elements::define();
|
||||||
|
|
||||||
let _app = seed::App::builder(update, view)
|
let app = seed::App::start("app", init, update, view);
|
||||||
.routes(routes)
|
|
||||||
.after_mount(after_mount)
|
{
|
||||||
.window_events(window_events)
|
let app_clone = app.clone();
|
||||||
.build_and_start();
|
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 host_url = unsafe { HOST_URL.clone() };
|
||||||
let ws_url = unsafe { WS_URL.clone() };
|
let ws_url = unsafe { WS_URL.clone() };
|
||||||
let mut model = Model::new(host_url, ws_url);
|
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);
|
model.page = resolve_page(url).unwrap_or(Page::Project);
|
||||||
open_socket(&mut model, orders);
|
open_socket(&mut model, orders);
|
||||||
AfterMount::new(model).url_handling(UrlHandling::PassToRoutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_events(_model: &Model) -> Vec<EventHandler<Msg>> {
|
// orders.subscribe(|subs::UrlChanged(url)| {
|
||||||
vec![keyboard_ev(
|
// if let Some(page) = resolve_page(url) {
|
||||||
Ev::KeyDown,
|
// orders.send_msg(Msg::ChangePage(page));
|
||||||
move |event: web_sys::KeyboardEvent| {
|
// }
|
||||||
let tag_name: String = seed::document()
|
// });
|
||||||
.active_element()
|
model
|
||||||
.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(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
use seed::prelude::Node;
|
use {
|
||||||
|
crate::{
|
||||||
use jirs_data::EpicId;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
model::{IssueModal, Model},
|
model::{IssueModal, Model},
|
||||||
shared::{styled_field::StyledField, styled_select::StyledSelect, ToChild, ToNode},
|
shared::{styled_field::StyledField, styled_select::StyledSelect, ToChild, ToNode},
|
||||||
FieldId, Msg,
|
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>>
|
pub fn epic_field<Modal>(model: &Model, modal: &Modal, field_id: FieldId) -> Option<Node<Msg>>
|
||||||
where
|
where
|
||||||
Modal: IssueModal,
|
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 jirs_data::{TimeTracking, WsMsg};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
modal::issues::*,
|
model::{self, ModalType, Model, Page},
|
||||||
model::{self, AddIssueModal, EditIssueModal, ModalType, Model, Page},
|
|
||||||
shared::{
|
shared::{
|
||||||
find_issue, go_to_board,
|
find_issue, go_to_board,
|
||||||
styled_confirm_modal::StyledConfirmModal,
|
styled_confirm_modal::StyledConfirmModal,
|
||||||
@ -18,7 +17,6 @@ use crate::{
|
|||||||
mod confirm_delete_issue;
|
mod confirm_delete_issue;
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
mod debug_modal;
|
mod debug_modal;
|
||||||
mod delete_issue_status;
|
|
||||||
pub mod issues;
|
pub mod issues;
|
||||||
pub mod time_tracking;
|
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) => {
|
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);
|
modal.project_id = model.project.as_ref().map(|p| p.id);
|
||||||
model.modals.push(ModalType::AddIssue(Box::new(modal)));
|
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)]
|
#[cfg(debug_assertions)]
|
||||||
Msg::GlobalKeyDown { key, .. } if key.eq(">") => {
|
Msg::GlobalKeyDown { key, .. } if key.eq(">") => {
|
||||||
|
orders.skip();
|
||||||
log!(model);
|
log!(model);
|
||||||
}
|
}
|
||||||
|
Msg::GlobalKeyDown { .. } => {
|
||||||
|
orders.skip();
|
||||||
|
}
|
||||||
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
add_issue::update(msg, model, orders);
|
|
||||||
issue_details::update(msg, model, orders);
|
use crate::modals::{issue_statuses_delete, issues_create, issues_edit};
|
||||||
delete_issue_status::update(msg, model, orders);
|
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> {
|
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||||
|
use crate::modals::{issue_statuses_delete, issues_create, issues_edit};
|
||||||
let modals: Vec<Node<Msg>> = model
|
let modals: Vec<Node<Msg>> = model
|
||||||
.modals
|
.modals
|
||||||
.iter()
|
.iter()
|
||||||
.map(|modal| match modal {
|
.map(|modal| match modal {
|
||||||
ModalType::EditIssue(issue_id, modal) => {
|
ModalType::EditIssue(issue_id, modal) => {
|
||||||
if let Some(_issue) = find_issue(model, *issue_id) {
|
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()
|
StyledModal::build()
|
||||||
.variant(ModalVariant::Center)
|
.variant(ModalVariant::Center)
|
||||||
.width(1040)
|
.width(1040)
|
||||||
@ -98,7 +103,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ModalType::DeleteIssueConfirm(_id) => confirm_delete_issue::view(model),
|
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) => {
|
ModalType::DeleteCommentConfirm(comment_id) => {
|
||||||
let comment_id = *comment_id;
|
let comment_id = *comment_id;
|
||||||
StyledConfirmModal::build()
|
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::TimeTracking(issue_id) => time_tracking::view(model, *issue_id),
|
||||||
ModalType::DeleteIssueStatusModal(delete_issue_modal) => {
|
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)]
|
#[cfg(debug_assertions)]
|
||||||
ModalType::DebugModal => debug_modal::view(model),
|
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(
|
ModalType::EditIssue(
|
||||||
issue_id,
|
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(
|
send_ws_msg(
|
||||||
|
@ -38,7 +38,7 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
|
|||||||
.map(|p| p.time_tracking)
|
.map(|p| p.time_tracking)
|
||||||
.unwrap_or_else(|| TimeTracking::Untracked);
|
.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);
|
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![
|
let inputs = div![
|
||||||
class!["inputs"],
|
C!["inputs"],
|
||||||
div![class!["inputContainer"], time_spent_field],
|
div![C!["inputContainer"], time_spent_field],
|
||||||
div![class!["inputContainer"], time_remaining_field]
|
div![C!["inputContainer"], time_remaining_field]
|
||||||
];
|
];
|
||||||
|
|
||||||
let close = StyledButton::build()
|
let close = StyledButton::build()
|
||||||
@ -75,7 +75,7 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
|
|||||||
modal_title,
|
modal_title,
|
||||||
tracking,
|
tracking,
|
||||||
inputs,
|
inputs,
|
||||||
div![class!["actions"], close],
|
div![C!["actions"], close],
|
||||||
])
|
])
|
||||||
.width(400)
|
.width(400)
|
||||||
.build()
|
.build()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
pub use model::*;
|
||||||
pub use update::*;
|
pub use update::*;
|
||||||
pub use view::*;
|
pub use view::*;
|
||||||
|
|
||||||
|
mod model;
|
||||||
mod update;
|
mod update;
|
||||||
mod view;
|
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 {
|
||||||
|
crate::{
|
||||||
use jirs_data::{IssueStatusId, WsMsg};
|
modals::issue_statuses_delete::Model as DeleteIssueStatusModal,
|
||||||
|
model::{ModalType, Model},
|
||||||
use crate::model::{DeleteIssueStatusModal, ModalType, Model};
|
Msg, WebSocketChanged,
|
||||||
use crate::shared::styled_confirm_modal::StyledConfirmModal;
|
},
|
||||||
use crate::shared::ToNode;
|
jirs_data::WsMsg,
|
||||||
use crate::{model, Msg, WebSocketChanged};
|
seed::prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
let _modal: &mut Box<DeleteIssueStatusModal> =
|
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 update::*;
|
||||||
pub use view::*;
|
pub use view::*;
|
||||||
|
|
||||||
|
mod model;
|
||||||
mod update;
|
mod update;
|
||||||
mod view;
|
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 {
|
||||||
|
crate::{
|
||||||
use jirs_data::{IssueFieldId, IssuePriority, ToVec, UserId, WsMsg};
|
|
||||||
|
|
||||||
use crate::shared::styled_date_time_input::StyledDateTimeInput;
|
|
||||||
use crate::{
|
|
||||||
modal::issues::epic_field,
|
modal::issues::epic_field,
|
||||||
model::{AddIssueModal, IssueModal, ModalType, Model},
|
modals::issues_create::{Model as AddIssueModal, Type},
|
||||||
|
model::Model,
|
||||||
shared::{
|
shared::{
|
||||||
styled_button::StyledButton,
|
styled_button::StyledButton, styled_date_time_input::StyledDateTimeInput,
|
||||||
styled_field::StyledField,
|
styled_field::StyledField, styled_form::StyledForm, styled_input::StyledInput,
|
||||||
styled_form::StyledForm,
|
styled_modal::StyledModal, styled_select::StyledSelect,
|
||||||
styled_input::StyledInput,
|
styled_textarea::StyledTextarea, ToChild, ToNode,
|
||||||
styled_modal::{StyledModal, Variant as ModalVariant},
|
|
||||||
styled_select::StyledSelect,
|
|
||||||
styled_select::StyledSelectChanged,
|
|
||||||
styled_select_child::{StyledSelectChild, StyledSelectChildBuilder},
|
|
||||||
styled_textarea::StyledTextarea,
|
|
||||||
ToChild, ToNode,
|
|
||||||
},
|
},
|
||||||
ws::send_ws_msg,
|
FieldId, Msg,
|
||||||
FieldId, Msg, WebSocketChanged,
|
},
|
||||||
|
jirs_data::{IssueFieldId, IssuePriority, ToVec},
|
||||||
|
seed::{prelude::*, *},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
pub fn view(model: &Model, modal: &Box<AddIssueModal>) -> Node<Msg> {
|
||||||
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> {
|
|
||||||
let issue_type = modal
|
let issue_type = modal
|
||||||
.type_state
|
.type_state
|
||||||
.values
|
.values
|
||||||
@ -270,8 +64,11 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
let reporter_field = reporter_field(model, modal);
|
let reporter_field = reporter_field(model, modal);
|
||||||
let assignees_field = assignees_field(model, modal);
|
let assignees_field = assignees_field(model, modal);
|
||||||
let issue_priority_field = issue_priority_field(modal);
|
let issue_priority_field = issue_priority_field(modal);
|
||||||
let epic_field =
|
let epic_field = epic_field(
|
||||||
epic_field(model, modal, FieldId::AddIssueModal(IssueFieldId::EpicName));
|
model,
|
||||||
|
modal.as_ref(),
|
||||||
|
FieldId::AddIssueModal(IssueFieldId::EpicName),
|
||||||
|
);
|
||||||
|
|
||||||
form.add_field(short_summary_field)
|
form.add_field(short_summary_field)
|
||||||
.add_field(description_field)
|
.add_field(description_field)
|
||||||
@ -317,13 +114,13 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
StyledModal::build()
|
StyledModal::build()
|
||||||
.add_class("addIssue")
|
.add_class("addIssue")
|
||||||
.width(0)
|
.width(0)
|
||||||
.variant(ModalVariant::Center)
|
.variant(crate::shared::styled_modal::Variant::Center)
|
||||||
.children(vec![form])
|
.children(vec![form])
|
||||||
.build()
|
.build()
|
||||||
.into_node()
|
.into_node()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
|
fn issue_type_field(modal: &Box<AddIssueModal>) -> Node<Msg> {
|
||||||
let select_type = StyledSelect::build()
|
let select_type = StyledSelect::build()
|
||||||
.name("type")
|
.name("type")
|
||||||
.normal()
|
.normal()
|
||||||
@ -351,7 +148,7 @@ fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
.into_node()
|
.into_node()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn short_summary_field(modal: &AddIssueModal) -> Node<Msg> {
|
fn short_summary_field(modal: &Box<AddIssueModal>) -> Node<Msg> {
|
||||||
let short_summary = StyledInput::build()
|
let short_summary = StyledInput::build()
|
||||||
.state(&modal.title_state)
|
.state(&modal.title_state)
|
||||||
.build(FieldId::AddIssueModal(IssueFieldId::Title))
|
.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 {
|
||||||
|
crate::{
|
||||||
use chrono::{prelude::*, NaiveDate};
|
pages::{
|
||||||
use seed::app::Orders;
|
invite_page::InvitePage, profile_page::model::ProfilePage,
|
||||||
use seed::browser::web_socket::WebSocket;
|
project_page::model::ProjectPage, project_settings_page::ProjectSettingsPage,
|
||||||
use serde::{Deserialize, Serialize};
|
reports_page::model::ReportsPage, sign_in_page::model::SignInPage,
|
||||||
use uuid::Uuid;
|
sign_up_page::model::SignUpPage, users_page::model::UsersPage,
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
EditIssueModalSection, FieldId, Msg, ProjectFieldId,
|
shared::styled_select::StyledSelectState,
|
||||||
|
Msg,
|
||||||
|
},
|
||||||
|
jirs_data::*,
|
||||||
|
seed::{app::Orders, browser::web_socket::WebSocket},
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
std::collections::hash_map::HashMap,
|
||||||
|
uuid::Uuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait IssueModal {
|
pub trait IssueModal {
|
||||||
fn epic_id_value(&self) -> Option<u32>;
|
fn epic_id_value(&self) -> Option<u32>;
|
||||||
|
|
||||||
fn epic_state(&self) -> &StyledSelectState;
|
fn epic_state(&self) -> &StyledSelectState;
|
||||||
|
|
||||||
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>);
|
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>);
|
||||||
@ -28,12 +26,12 @@ pub trait IssueModal {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||||
pub enum ModalType {
|
pub enum ModalType {
|
||||||
AddIssue(Box<AddIssueModal>),
|
AddIssue(Box<crate::modals::issues_create::Model>),
|
||||||
EditIssue(IssueId, Box<EditIssueModal>),
|
EditIssue(IssueId, Box<crate::modals::issues_edit::Model>),
|
||||||
DeleteIssueConfirm(IssueId),
|
DeleteIssueConfirm(IssueId),
|
||||||
DeleteCommentConfirm(CommentId),
|
DeleteCommentConfirm(CommentId),
|
||||||
TimeTracking(IssueId),
|
TimeTracking(IssueId),
|
||||||
DeleteIssueStatusModal(Box<DeleteIssueStatusModal>),
|
DeleteIssueStatusModal(Box<crate::modals::issue_statuses_delete::Model>),
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
DebugModal,
|
DebugModal,
|
||||||
}
|
}
|
||||||
@ -45,259 +43,6 @@ pub struct CommentForm {
|
|||||||
pub creating: bool,
|
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)]
|
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
|
||||||
pub enum Page {
|
pub enum Page {
|
||||||
Project,
|
Project,
|
||||||
@ -345,109 +90,6 @@ pub struct UpdateProjectForm {
|
|||||||
pub fields: UpdateProjectPayload,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialOrd, PartialEq)]
|
||||||
pub enum InvitationFormState {
|
pub enum InvitationFormState {
|
||||||
Initial = 1,
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum PageContent {
|
pub enum PageContent {
|
||||||
SignIn(Box<SignInPage>),
|
SignIn(Box<SignInPage>),
|
||||||
@ -591,12 +143,22 @@ pub struct Model {
|
|||||||
pub page_content: PageContent,
|
pub page_content: PageContent,
|
||||||
|
|
||||||
pub project: Option<Project>,
|
pub project: Option<Project>,
|
||||||
pub user: Option<User>,
|
|
||||||
pub current_user_project: Option<UserProject>,
|
pub current_user_project: Option<UserProject>,
|
||||||
|
|
||||||
pub issues: Vec<Issue>,
|
pub issues: Vec<Issue>,
|
||||||
|
pub issues_by_id: HashMap<IssueId, Issue>,
|
||||||
|
|
||||||
|
pub user: Option<User>,
|
||||||
pub users: Vec<User>,
|
pub users: Vec<User>,
|
||||||
|
pub users_by_id: HashMap<UserId, User>,
|
||||||
|
|
||||||
pub comments: Vec<Comment>,
|
pub comments: Vec<Comment>,
|
||||||
|
|
||||||
pub issue_statuses: Vec<IssueStatus>,
|
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 messages: Vec<Message>,
|
||||||
pub user_projects: Vec<UserProject>,
|
pub user_projects: Vec<UserProject>,
|
||||||
pub projects: Vec<Project>,
|
pub projects: Vec<Project>,
|
||||||
@ -605,8 +167,6 @@ pub struct Model {
|
|||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
pub fn new(host_url: String, ws_url: String) -> Self {
|
pub fn new(host_url: String, ws_url: String) -> Self {
|
||||||
// let hi_worker = Worker::new("/hi.js");
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ws: None,
|
ws: None,
|
||||||
ws_queue: vec![],
|
ws_queue: vec![],
|
||||||
@ -627,12 +187,16 @@ impl Model {
|
|||||||
messages_tooltip_visible: false,
|
messages_tooltip_visible: false,
|
||||||
issues: vec![],
|
issues: vec![],
|
||||||
users: vec![],
|
users: vec![],
|
||||||
|
users_by_id: Default::default(),
|
||||||
comments: vec![],
|
comments: vec![],
|
||||||
issue_statuses: vec![],
|
issue_statuses: vec![],
|
||||||
|
issue_statuses_by_id: Default::default(),
|
||||||
|
issue_statuses_by_name: Default::default(),
|
||||||
messages: vec![],
|
messages: vec![],
|
||||||
user_projects: vec![],
|
user_projects: vec![],
|
||||||
projects: vec![],
|
projects: vec![],
|
||||||
epics: vec![],
|
epics: vec![],
|
||||||
|
issues_by_id: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,10 +206,4 @@ impl Model {
|
|||||||
.map(|up| up.role)
|
.map(|up| up.role)
|
||||||
.unwrap_or_default()
|
.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 {
|
||||||
use web_sys::FormData;
|
crate::{
|
||||||
|
model::{Model, Page, PageContent},
|
||||||
use jirs_data::{ProjectId, UsersFieldId, WsMsg};
|
pages::profile_page::model::ProfilePage,
|
||||||
|
shared::styled_select::StyledSelectChanged,
|
||||||
use crate::model::{Model, Page, PageContent, ProfilePage};
|
ws::{board_load, send_ws_msg},
|
||||||
use crate::shared::styled_select::StyledSelectChanged;
|
FieldId, Msg, OperationKind, PageChanged, ProfilePageChange, ResourceKind,
|
||||||
use crate::ws::{board_load, send_ws_msg};
|
WebSocketChanged,
|
||||||
use crate::{FieldId, Msg, PageChanged, ProfilePageChange, 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>) {
|
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
|
Msg::ResourceChanged(ResourceKind::Auth, OperationKind::SingleLoaded, Some(_))
|
||||||
| Msg::ChangePage(Page::Profile) => {
|
| Msg::ChangePage(Page::Profile) => {
|
||||||
board_load(model, orders);
|
board_load(model, orders);
|
||||||
build_page_content(model);
|
build_page_content(model);
|
||||||
@ -45,15 +49,16 @@ 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.perform_cmd(update_avatar(fd, model.host_url.clone()));
|
||||||
orders.skip();
|
orders.skip();
|
||||||
}
|
}
|
||||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(ws_msg)) => {
|
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AvatarUrlChanged(
|
||||||
if let WsMsg::AvatarUrlChanged(user_id, avatar_url) = ws_msg {
|
user_id,
|
||||||
if let Some(me) = model.user.as_mut() {
|
avatar_url,
|
||||||
if me.id == user_id {
|
))) => {
|
||||||
|
if let Some(User { id, .. }) = model.user.as_mut() {
|
||||||
|
if *id == user_id {
|
||||||
profile_page.avatar.url = Some(avatar_url);
|
profile_page.avatar.url = Some(avatar_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Msg::ProjectChanged(Some(project)) => {
|
Msg::ProjectChanged(Some(project)) => {
|
||||||
profile_page.current_project.values = vec![project.id as u32];
|
profile_page.current_project.values = vec![project.id as u32];
|
||||||
}
|
}
|
@ -1,18 +1,18 @@
|
|||||||
use std::collections::HashMap;
|
use {
|
||||||
|
crate::{
|
||||||
use seed::{prelude::*, *};
|
model::{Model, PageContent},
|
||||||
|
pages::profile_page::model::ProfilePage,
|
||||||
use jirs_data::*;
|
shared::{
|
||||||
|
inner_layout, styled_button::StyledButton, styled_field::StyledField,
|
||||||
use crate::model::{Model, PageContent, ProfilePage};
|
styled_form::StyledForm, styled_image_input::StyledImageInput,
|
||||||
use crate::shared::styled_button::StyledButton;
|
styled_input::StyledInput, styled_select::StyledSelect, ToChild, ToNode,
|
||||||
use crate::shared::styled_field::StyledField;
|
},
|
||||||
use crate::shared::styled_form::StyledForm;
|
FieldId, Msg, PageChanged, ProfilePageChange,
|
||||||
use crate::shared::styled_image_input::StyledImageInput;
|
},
|
||||||
use crate::shared::styled_input::StyledInput;
|
jirs_data::*,
|
||||||
use crate::shared::styled_select::StyledSelect;
|
seed::{prelude::*, *},
|
||||||
use crate::shared::{inner_layout, ToChild, ToNode};
|
std::collections::HashMap,
|
||||||
use crate::{FieldId, Msg, PageChanged, ProfilePageChange};
|
};
|
||||||
|
|
||||||
pub fn view(model: &Model) -> Node<Msg> {
|
pub fn view(model: &Model) -> Node<Msg> {
|
||||||
let page = match &model.page_content {
|
let page = match &model.page_content {
|
||||||
@ -122,7 +122,7 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
|
|||||||
};
|
};
|
||||||
StyledField::build()
|
StyledField::build()
|
||||||
.label("Current project")
|
.label("Current project")
|
||||||
.input(div![class!["project-name"], inner])
|
.input(div![C!["project-name"], inner])
|
||||||
.build()
|
.build()
|
||||||
.into_node()
|
.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::{OperationKind, ResourceKind};
|
||||||
|
|
||||||
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};
|
|
||||||
|
|
||||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||||
if model.user.is_none() {
|
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 {
|
match msg {
|
||||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
|
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
|
||||||
|
| Msg::UserChanged(..)
|
||||||
| Msg::ProjectChanged(Some(..))
|
| Msg::ProjectChanged(Some(..))
|
||||||
| Msg::ChangePage(Page::Project)
|
| Msg::ChangePage(Page::Project)
|
||||||
| Msg::ChangePage(Page::AddIssue)
|
| Msg::ChangePage(Page::AddIssue)
|
||||||
| Msg::ChangePage(Page::EditIssue(..)) => {
|
| Msg::ChangePage(Page::EditIssue(..)) => {
|
||||||
board_load(model, orders);
|
board_load(model, orders);
|
||||||
}
|
}
|
||||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
|
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleRemoved, ..) => {
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
orders.skip().send_msg(Msg::ModalDropped);
|
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(
|
Msg::StyledSelectChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
@ -1,15 +1,19 @@
|
|||||||
use chrono::NaiveDateTime;
|
use {
|
||||||
use seed::{prelude::*, *};
|
crate::{
|
||||||
|
model::{Model, Page, PageContent},
|
||||||
use jirs_data::*;
|
shared::{
|
||||||
|
inner_layout,
|
||||||
use crate::model::{Model, PageContent};
|
styled_avatar::StyledAvatar,
|
||||||
use crate::shared::styled_avatar::StyledAvatar;
|
styled_button::StyledButton,
|
||||||
use crate::shared::styled_button::StyledButton;
|
styled_icon::{Icon, StyledIcon},
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
styled_input::StyledInput,
|
||||||
use crate::shared::styled_input::StyledInput;
|
ToNode,
|
||||||
use crate::shared::{inner_layout, ToNode};
|
},
|
||||||
use crate::{BoardPageChange, FieldId, Msg, PageChanged};
|
BoardPageChange, FieldId, Msg, PageChanged,
|
||||||
|
},
|
||||||
|
jirs_data::*,
|
||||||
|
seed::{prelude::*, *},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn view(model: &Model) -> Node<Msg> {
|
pub fn view(model: &Model) -> Node<Msg> {
|
||||||
let project_section = vec![
|
let project_section = vec![
|
||||||
@ -29,11 +33,11 @@ fn breadcrumbs(model: &Model) -> Node<Msg> {
|
|||||||
.map(|p| p.name.clone())
|
.map(|p| p.name.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
div![
|
div![
|
||||||
class!["breadcrumbsContainer"],
|
C!["breadcrumbsContainer"],
|
||||||
span!["Projects"],
|
span!["Projects"],
|
||||||
span![class!["breadcrumbsDivider"], "/"],
|
span![C!["breadcrumbsDivider"], "/"],
|
||||||
span![project_name],
|
span![project_name],
|
||||||
span![class!["breadcrumbsDivider"], "/"],
|
span![C!["breadcrumbsDivider"], "/"],
|
||||||
span!["Kanban Board"]
|
span!["Kanban Board"]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -47,7 +51,7 @@ fn header() -> Node<Msg> {
|
|||||||
.into_node();
|
.into_node();
|
||||||
div![
|
div![
|
||||||
id!["projectBoardHeader"],
|
id!["projectBoardHeader"],
|
||||||
div![id!["boardName"], class!["headerChild"], "Kanban board"],
|
div![id!["boardName"], C!["headerChild"], "Kanban board"],
|
||||||
a![
|
a![
|
||||||
attrs![At::Href => "https://gitlab.com/adrian.wozniak/jirs", At::Target => "__blank", At::Rel => "noreferrer noopener"],
|
attrs![At::Href => "https://gitlab.com/adrian.wozniak/jirs", At::Target => "__blank", At::Rel => "noreferrer noopener"],
|
||||||
button
|
button
|
||||||
@ -91,7 +95,7 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
|
|||||||
{
|
{
|
||||||
seed::button![
|
seed::button![
|
||||||
id!["clearAllFilters"],
|
id!["clearAllFilters"],
|
||||||
class!["filterChild"],
|
C!["filterChild"],
|
||||||
"Clear all",
|
"Clear all",
|
||||||
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
|
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
|
||||||
]
|
]
|
||||||
@ -139,94 +143,77 @@ fn avatars_filters(model: &Model) -> Node<Msg> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
div![id!["avatars"], class!["filterChild"], avatars]
|
div![id!["avatars"], C!["filterChild"], avatars]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project_board_lists(model: &Model) -> Node<Msg> {
|
fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||||
let mut rows: Vec<Option<&Epic>> = vec![None];
|
let project_page = match &model.page_content {
|
||||||
for epic in model.epics.iter() {
|
PageContent::Project(project_page) => project_page,
|
||||||
rows.push(Some(epic));
|
_ => return empty![],
|
||||||
}
|
};
|
||||||
let rows: Vec<Node<Msg>> = rows
|
let rows = project_page.visible_issues.iter().map(|per_epic| {
|
||||||
.into_iter()
|
let columns: Vec<Node<Msg>> = per_epic
|
||||||
.map(|epic| {
|
.per_status_issues
|
||||||
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()
|
.iter()
|
||||||
.map(|issue_status| project_issue_list(model, issue_status, epic))
|
.map(|per_status| {
|
||||||
|
let issues: Vec<&Issue> = per_status
|
||||||
|
.issue_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| model.issues_by_id.get(id))
|
||||||
.collect();
|
.collect();
|
||||||
div![C!["row"], title, div![C!["projectBoardLists"], columns]]
|
project_issue_list(
|
||||||
|
model,
|
||||||
|
per_status.status_id,
|
||||||
|
&per_status.status_name,
|
||||||
|
issues.as_slice(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
div![
|
||||||
|
C!["row"],
|
||||||
|
div![C!["epicName"], per_epic.epic_name.as_str()],
|
||||||
|
div![C!["projectBoardLists"], columns]
|
||||||
|
]
|
||||||
|
});
|
||||||
div![C!["rows"], rows]
|
div![C!["rows"], rows]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project_issue_list(
|
fn project_issue_list(
|
||||||
model: &Model,
|
model: &Model,
|
||||||
status: &jirs_data::IssueStatus,
|
status_id: IssueStatusId,
|
||||||
epic: Option<&jirs_data::Epic>,
|
status_name: &str,
|
||||||
|
issues: &[&Issue],
|
||||||
) -> Node<Msg> {
|
) -> Node<Msg> {
|
||||||
let project_page = match &model.page_content {
|
let issues: Vec<Node<Msg>> = issues
|
||||||
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()
|
.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
|
|
||||||
.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))
|
.map(|issue| project_issue(model, issue))
|
||||||
.collect();
|
.collect();
|
||||||
let label = status.name.clone();
|
let drop_handler = {
|
||||||
|
let send_status = status_id;
|
||||||
let send_status = status.id;
|
drag_ev(Ev::Drop, move |ev| {
|
||||||
let drop_handler = drag_ev(Ev::Drop, move |ev| {
|
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
BoardPageChange::IssueDropZone(send_status),
|
BoardPageChange::IssueDropZone(send_status),
|
||||||
)))
|
)))
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let send_status = status.id;
|
let drag_over_handler = {
|
||||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
let send_status = status_id;
|
||||||
|
drag_ev(Ev::DragOver, move |ev| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
BoardPageChange::IssueDragOverStatus(send_status),
|
BoardPageChange::IssueDragOverStatus(send_status),
|
||||||
)))
|
)))
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
div![
|
div![
|
||||||
attrs![At::Class => "list";],
|
C!["list"],
|
||||||
|
div![C!["title"], status_name, div![C!["issuesCount"]]],
|
||||||
div![
|
div![
|
||||||
attrs![At::Class => "title"],
|
C!["issues"],
|
||||||
label,
|
attrs![At::DropZone => "link"],
|
||||||
div![attrs![At::Class => "issuesCount"]]
|
|
||||||
],
|
|
||||||
div![
|
|
||||||
attrs![At::Class => "issues"; At::DropZone => "link"],
|
|
||||||
drop_handler,
|
drop_handler,
|
||||||
drag_over_handler,
|
drag_over_handler,
|
||||||
issues
|
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> {
|
fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||||
let avatars: Vec<Node<Msg>> = model
|
let avatars: Vec<Node<Msg>> = issue
|
||||||
.users
|
.user_ids
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.filter_map(|id| model.users_by_id.get(id))
|
||||||
.filter(|(_, user)| issue.user_ids.contains(&user.id))
|
.map(|user| {
|
||||||
.map(|(idx, user)| {
|
|
||||||
StyledAvatar::build()
|
StyledAvatar::build()
|
||||||
.size(24)
|
.size(24)
|
||||||
.name(user.name.as_str())
|
.name(user.name.as_str())
|
||||||
.avatar_url(user.avatar_url.as_deref().unwrap_or_default())
|
.avatar_url(user.avatar_url.as_deref().unwrap_or_default())
|
||||||
.user_index(idx)
|
.user_index(0)
|
||||||
.build()
|
.build()
|
||||||
.into_node()
|
.into_node()
|
||||||
})
|
})
|
||||||
.collect();
|
.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 = {
|
let issue_type_icon = StyledIcon::build(issue.issue_type.clone().into())
|
||||||
StyledIcon::build(issue.issue_type.clone().into())
|
|
||||||
.with_color(issue.issue_type.to_str())
|
.with_color(issue.issue_type.to_str())
|
||||||
.build()
|
.build()
|
||||||
.into_node()
|
.into_node();
|
||||||
};
|
|
||||||
let priority_icon = {
|
let priority_icon = {
|
||||||
let icon = match issue.priority {
|
let icon = match issue.priority {
|
||||||
IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown,
|
IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown,
|
||||||
@ -315,33 +285,43 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
|||||||
BoardPageChange::ExchangePosition(issue_id),
|
BoardPageChange::ExchangePosition(issue_id),
|
||||||
)))
|
)))
|
||||||
});
|
});
|
||||||
let issue_id = issue.id;
|
|
||||||
let drag_out = drag_ev(Ev::DragLeave, move |_| {
|
let drag_out = drag_ev(Ev::DragLeave, move |_| {
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
BoardPageChange::DragLeave(issue_id),
|
BoardPageChange::DragLeave(issue_id),
|
||||||
)))
|
)))
|
||||||
});
|
});
|
||||||
|
let on_click = mouse_ev("click", move |ev| {
|
||||||
let class_list = vec!["issue"];
|
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);
|
let href = format!("/issues/{id}", id = issue_id);
|
||||||
|
|
||||||
a![
|
a![
|
||||||
drag_started,
|
drag_started,
|
||||||
attrs![At::Class => "issueLink"; At::Href => href],
|
on_click,
|
||||||
|
C!["issueLink"],
|
||||||
|
attrs![At::Href => href],
|
||||||
div![
|
div![
|
||||||
attrs![At::Class => class_list.join(" "), At::Draggable => true],
|
C!["issue"],
|
||||||
|
attrs![At::Draggable => true],
|
||||||
drag_stopped,
|
drag_stopped,
|
||||||
drag_over_handler,
|
drag_over_handler,
|
||||||
drag_out,
|
drag_out,
|
||||||
p![attrs![At::Class => "title"], issue.title.as_str()],
|
p![C!["title"], issue.title.as_str()],
|
||||||
div![
|
div![
|
||||||
attrs![At::Class => "bottom"],
|
C!["bottom"],
|
||||||
div![
|
div![
|
||||||
div![attrs![At::Class => "issueTypeIcon"], issue_type_icon],
|
div![C!["issueTypeIcon"], issue_type_icon],
|
||||||
div![attrs![At::Class => "issuePriorityIcon"], priority_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 crate::pages::project_settings_page::ProjectSettingsPage;
|
||||||
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};
|
|
||||||
|
|
||||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
if model.page != Page::ProjectSettings {
|
if model.page != Page::ProjectSettings {
|
@ -4,19 +4,27 @@ use seed::{prelude::*, *};
|
|||||||
|
|
||||||
use jirs_data::{IssueStatus, ProjectCategory, TimeTracking, ToVec};
|
use jirs_data::{IssueStatus, ProjectCategory, TimeTracking, ToVec};
|
||||||
|
|
||||||
use crate::model::{DeleteIssueStatusModal, ModalType, Model, PageContent, ProjectSettingsPage};
|
use crate::{
|
||||||
use crate::shared::styled_button::StyledButton;
|
modals::issue_statuses_delete::Model as DeleteIssueStatusModal,
|
||||||
use crate::shared::styled_checkbox::StyledCheckbox;
|
model::{self, ModalType, Model, PageContent},
|
||||||
use crate::shared::styled_editor::StyledEditor;
|
pages::project_settings_page::ProjectSettingsPage,
|
||||||
use crate::shared::styled_field::StyledField;
|
shared::{
|
||||||
use crate::shared::styled_form::StyledForm;
|
inner_layout,
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
styled_button::StyledButton,
|
||||||
use crate::shared::styled_input::StyledInput;
|
styled_checkbox::StyledCheckbox,
|
||||||
use crate::shared::{inner_layout, ToChild, ToNode};
|
styled_editor::StyledEditor,
|
||||||
use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange};
|
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_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_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt");
|
||||||
static TIME_TRACKING_HOURLY: &str = include_str!("./time_tracking_hourly.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()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
let project_section = vec![div![class!["formContainer"], form]];
|
let project_section = vec![div![C!["formContainer"], form]];
|
||||||
|
|
||||||
inner_layout(model, "projectSettings", project_section)
|
inner_layout(model, "projectSettings", project_section)
|
||||||
}
|
}
|
||||||
@ -201,9 +209,9 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let columns_section = section![
|
let columns_section = section![
|
||||||
class!["columnsSection"],
|
C!["columnsSection"],
|
||||||
div![
|
div![
|
||||||
class!["columns"],
|
C!["columns"],
|
||||||
columns,
|
columns,
|
||||||
add_column(page, column_style.as_str())
|
add_column(page, column_style.as_str())
|
||||||
]
|
]
|
||||||
@ -246,15 +254,15 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
|
|||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class!["columnPreview"],
|
C!["columnPreview"],
|
||||||
div![class!["columnName"], form![on_submit, input]]
|
div![C!["columnName"], form![on_submit, input]]
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
let add_column = StyledIcon::build(Icon::Plus).build().into_node();
|
let add_column = StyledIcon::build(Icon::Plus).build().into_node();
|
||||||
div![
|
div![
|
||||||
class!["columnPreview"],
|
C!["columnPreview"],
|
||||||
attrs![At::Style => column_style],
|
attrs![At::Style => column_style],
|
||||||
div![class!["columnName addColumn"], add_column],
|
div![C!["columnName addColumn"], add_column],
|
||||||
on_click,
|
on_click,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -280,7 +288,7 @@ fn column_preview(
|
|||||||
.build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
|
.build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
div![class!["columnPreview"], div![class!["columnName"], input]]
|
div![C!["columnPreview"], div![C!["columnName"], input]]
|
||||||
} else {
|
} else {
|
||||||
show_column_preview(is, per_column_issue_count, column_style)
|
show_column_preview(is, per_column_issue_count, column_style)
|
||||||
}
|
}
|
||||||
@ -336,19 +344,19 @@ fn show_column_preview(
|
|||||||
.on_click(on_delete)
|
.on_click(on_delete)
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
div![class!["removeColumn"], delete]
|
div![C!["removeColumn"], delete]
|
||||||
} else {
|
} else {
|
||||||
div![
|
div![
|
||||||
class!["issueCount"],
|
C!["issueCount"],
|
||||||
format!("Issues in column: {}", issue_count_in_column)
|
format!("Issues in column: {}", issue_count_in_column)
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class!["columnPreview"],
|
C!["columnPreview"],
|
||||||
attrs![At::Style => column_style, At::Draggable => "true", At::DropZone => "true"],
|
attrs![At::Style => column_style, At::Draggable => "true", At::DropZone => "true"],
|
||||||
div![
|
div![
|
||||||
class!["columnName"],
|
C!["columnName"],
|
||||||
span![is.name.as_str()],
|
span![is.name.as_str()],
|
||||||
on_edit,
|
on_edit,
|
||||||
delete_row
|
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 jirs_data::WsMsg;
|
||||||
|
|
||||||
use crate::changes::{PageChanged, ReportsPageChange};
|
use crate::pages::reports_page::model::ReportsPage;
|
||||||
use crate::model::{Model, Page, PageContent, ReportsPage};
|
use crate::{
|
||||||
use crate::ws::board_load;
|
changes::{PageChanged, ReportsPageChange},
|
||||||
use crate::{Msg, WebSocketChanged};
|
model::{Model, Page, PageContent},
|
||||||
|
ws::board_load,
|
||||||
|
Msg, WebSocketChanged,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||||
if let Msg::ChangePage(Page::Reports) = msg {
|
if let Msg::ChangePage(Page::Reports) = msg {
|
@ -5,10 +5,12 @@ use seed::{prelude::*, *};
|
|||||||
|
|
||||||
use jirs_data::Issue;
|
use jirs_data::Issue;
|
||||||
|
|
||||||
use crate::model::{Model, PageContent, ReportsPage};
|
use crate::pages::reports_page::model::ReportsPage;
|
||||||
use crate::shared::styled_icon::StyledIcon;
|
use crate::{
|
||||||
use crate::shared::{inner_layout, ToNode};
|
model::{Model, PageContent},
|
||||||
use crate::{Msg, PageChanged, ReportsPageChange};
|
shared::{inner_layout, styled_icon::StyledIcon, ToNode},
|
||||||
|
Msg, PageChanged, ReportsPageChange,
|
||||||
|
};
|
||||||
|
|
||||||
const SVG_MARGIN_X: u32 = 10;
|
const SVG_MARGIN_X: u32 = 10;
|
||||||
const SVG_DRAWABLE_HEIGHT: u32 = 300;
|
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 graph = this_month_graph(page, &this_month_updated);
|
||||||
let list = issue_list(page, this_month_updated.as_slice());
|
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])
|
inner_layout(model, "reports", vec![body])
|
||||||
}
|
}
|
||||||
@ -138,8 +140,8 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<M
|
|||||||
}
|
}
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class!["graph"],
|
C!["graph"],
|
||||||
h5![class!["graphHeader"], "Last updated"],
|
h5![C!["graphHeader"], "Last updated"],
|
||||||
svg![
|
svg![
|
||||||
attrs![At::Height => SVG_HEIGHT, At::Width => SVG_WIDTH],
|
attrs![At::Height => SVG_HEIGHT, At::Width => SVG_WIDTH],
|
||||||
svg_parts,
|
svg_parts,
|
||||||
@ -173,21 +175,21 @@ fn issue_list(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<Msg> {
|
|||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
children.push(li![
|
children.push(li![
|
||||||
class!["issue"],
|
C!["issue"],
|
||||||
class![active_class],
|
C![active_class],
|
||||||
span![class!["priority"], priority_icon],
|
span![C!["priority"], priority_icon],
|
||||||
span![class!["type"], type_icon],
|
span![C!["type"], type_icon],
|
||||||
span![class!["name"], title.as_str()],
|
span![C!["name"], title.as_str()],
|
||||||
span![
|
span![
|
||||||
class!["desc"],
|
C!["desc"],
|
||||||
description.as_ref().cloned().unwrap_or_default()
|
description.as_ref().cloned().unwrap_or_default()
|
||||||
],
|
],
|
||||||
span![class!["updatedAt"], day.as_str()],
|
span![C!["updatedAt"], day.as_str()],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
div![
|
div![
|
||||||
class!["issueList"],
|
C!["issueList"],
|
||||||
h5![class!["issueListHeader"], "Issues this month"],
|
h5![C!["issueListHeader"], "Issues this month"],
|
||||||
children
|
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 seed::{prelude::*, *};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use jirs_data::WsMsg;
|
use crate::{
|
||||||
|
model::{self, PageContent},
|
||||||
use crate::model::{Model, Page, PageContent, SignInPage};
|
shared::{
|
||||||
use crate::shared::styled_button::StyledButton;
|
outer_layout,
|
||||||
use crate::shared::styled_field::StyledField;
|
styled_button::StyledButton,
|
||||||
use crate::shared::styled_form::StyledForm;
|
styled_field::StyledField,
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
styled_form::StyledForm,
|
||||||
use crate::shared::styled_input::StyledInput;
|
styled_icon::{Icon, StyledIcon},
|
||||||
use crate::shared::styled_link::StyledLink;
|
styled_input::StyledInput,
|
||||||
use crate::shared::{outer_layout, write_auth_token, ToNode};
|
styled_link::StyledLink,
|
||||||
use crate::validations::{is_email, is_token};
|
ToNode,
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
},
|
},
|
||||||
_ => (),
|
validations::{is_email, is_token},
|
||||||
};
|
FieldId, Msg, SignInFieldId,
|
||||||
}
|
};
|
||||||
|
|
||||||
fn build_page_content(model: &mut Model) {
|
|
||||||
model.page_content = PageContent::SignIn(Box::new(SignInPage::default()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||||
let page = match &model.page_content {
|
let page = match &model.page_content {
|
||||||
@ -133,7 +63,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
let submit_field = StyledField::build()
|
let submit_field = StyledField::build()
|
||||||
.input(div![class!["twoRow"], submit, register_link,])
|
.input(div![C!["twoRow"], submit, register_link,])
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
@ -144,7 +74,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
let no_pass_section = div![
|
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."],
|
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,
|
help_icon,
|
||||||
span!["Why I don't see password?"]
|
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 seed::{prelude::*, *};
|
||||||
|
|
||||||
use jirs_data::{SignUpFieldId, WsMsg};
|
use jirs_data::SignUpFieldId;
|
||||||
|
|
||||||
use crate::model::{Model, Page, PageContent, SignUpPage};
|
use crate::{
|
||||||
use crate::shared::styled_button::StyledButton;
|
model::{self, PageContent},
|
||||||
use crate::shared::styled_field::StyledField;
|
shared::{
|
||||||
use crate::shared::styled_form::StyledForm;
|
outer_layout,
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
styled_button::StyledButton,
|
||||||
use crate::shared::styled_input::StyledInput;
|
styled_field::StyledField,
|
||||||
use crate::shared::styled_link::StyledLink;
|
styled_form::StyledForm,
|
||||||
use crate::shared::{outer_layout, ToNode};
|
styled_icon::{Icon, StyledIcon},
|
||||||
use crate::validations::is_email;
|
styled_input::StyledInput,
|
||||||
use crate::ws::send_ws_msg;
|
styled_link::StyledLink,
|
||||||
use crate::{model, FieldId, Msg, WebSocketChanged};
|
ToNode,
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
},
|
},
|
||||||
_ => (),
|
validations::is_email,
|
||||||
}
|
FieldId, Msg,
|
||||||
}
|
};
|
||||||
|
|
||||||
fn build_page_content(model: &mut Model) {
|
|
||||||
model.page_content = PageContent::SignUp(Box::new(SignUpPage::default()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||||
let page = match &model.page_content {
|
let page = match &model.page_content {
|
||||||
@ -111,7 +67,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
let submit_field = StyledField::build()
|
let submit_field = StyledField::build()
|
||||||
.input(div![class!["twoRow"], submit, sign_in_link,])
|
.input(div![C!["twoRow"], submit, sign_in_link,])
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
@ -122,7 +78,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
let no_pass_section = div![
|
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."],
|
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,
|
help_icon,
|
||||||
span!["Why I don't see password?"]
|
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() {
|
let error_row = if page.error.is_empty() {
|
||||||
empty![]
|
empty![]
|
||||||
} else {
|
} else {
|
||||||
div![class!["error"], p![page.error.as_str()]]
|
div![C!["error"], p![page.error.as_str()]]
|
||||||
};
|
};
|
||||||
|
|
||||||
let sign_up_form = StyledForm::build()
|
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 jirs_data::{InvitationState, UserRole, UsersFieldId, WsMsg};
|
||||||
|
|
||||||
use crate::model::{InvitationFormState, Model, Page, PageContent, UsersPage};
|
use crate::pages::users_page::model::UsersPage;
|
||||||
use crate::shared::styled_select::StyledSelectChanged;
|
use crate::{
|
||||||
use crate::ws::{invitation_load, send_ws_msg};
|
model::{InvitationFormState, Model, Page, PageContent},
|
||||||
use crate::{FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged};
|
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>) {
|
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
if let Msg::ChangePage(Page::Users) = msg {
|
if let Msg::ChangePage(Page::Users) = msg {
|
@ -2,15 +2,16 @@ use seed::{prelude::*, *};
|
|||||||
|
|
||||||
use jirs_data::{InvitationState, ToVec, UserRole, UsersFieldId};
|
use jirs_data::{InvitationState, ToVec, UserRole, UsersFieldId};
|
||||||
|
|
||||||
use crate::model::{InvitationFormState, Model, PageContent};
|
use crate::{
|
||||||
use crate::shared::styled_button::StyledButton;
|
model::{InvitationFormState, Model, PageContent},
|
||||||
use crate::shared::styled_field::StyledField;
|
shared::{
|
||||||
use crate::shared::styled_form::StyledForm;
|
inner_layout, styled_button::StyledButton, styled_field::StyledField,
|
||||||
use crate::shared::styled_input::StyledInput;
|
styled_form::StyledForm, styled_input::StyledInput, styled_select::StyledSelect, ToChild,
|
||||||
use crate::shared::styled_select::StyledSelect;
|
ToNode,
|
||||||
use crate::shared::{inner_layout, ToChild, ToNode};
|
},
|
||||||
use crate::validations::is_email;
|
validations::is_email,
|
||||||
use crate::{FieldId, Msg, PageChanged, UsersPageChange};
|
FieldId, Msg, PageChanged, UsersPageChange,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn view(model: &Model) -> Node<Msg> {
|
pub fn view(model: &Model) -> Node<Msg> {
|
||||||
if model.user.is_none() {
|
if model.user.is_none() {
|
||||||
@ -79,11 +80,11 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
.text("Reset")
|
.text("Reset")
|
||||||
.build()
|
.build()
|
||||||
.into_node(),
|
.into_node(),
|
||||||
InvitationFormState::Failed => div![class!["error"], "There was an error"],
|
InvitationFormState::Failed => div![C!["error"], "There was an error"],
|
||||||
_ => empty![],
|
_ => empty![],
|
||||||
};
|
};
|
||||||
let submit_field = StyledField::build()
|
let submit_field = StyledField::build()
|
||||||
.input(div![class!["invitationActions"], submit, submit_supplement])
|
.input(div![C!["invitationActions"], submit, submit_supplement])
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
@ -120,7 +121,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
li![
|
li![
|
||||||
class!["user"],
|
C!["user"],
|
||||||
span![user.name.as_str()],
|
span![user.name.as_str()],
|
||||||
span![user.email.as_str()],
|
span![user.email.as_str()],
|
||||||
span![format!("{}", role)],
|
span![format!("{}", role)],
|
||||||
@ -130,9 +131,9 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let users_section = section![
|
let users_section = section![
|
||||||
class!["usersSection"],
|
C!["usersSection"],
|
||||||
h1![class!["heading"], "Users"],
|
h1![C!["heading"], "Users"],
|
||||||
ul![class!["usersList"], users],
|
ul![C!["usersList"], users],
|
||||||
];
|
];
|
||||||
|
|
||||||
let invitations: Vec<Node<Msg>> = page
|
let invitations: Vec<Node<Msg>> = page
|
||||||
@ -147,7 +148,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
li![
|
li![
|
||||||
class!["invitation"],
|
C!["invitation"],
|
||||||
attrs![At::Class => format!("{}", invitation.state)],
|
attrs![At::Class => format!("{}", invitation.state)],
|
||||||
span![invitation.name.as_str()],
|
span![invitation.name.as_str()],
|
||||||
span![invitation.email.as_str()],
|
span![invitation.email.as_str()],
|
||||||
@ -158,9 +159,9 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let invitations_section = section![
|
let invitations_section = section![
|
||||||
class!["invitationsSection"],
|
C!["invitationsSection"],
|
||||||
h1![class!["heading"], "Invitations"],
|
h1![C!["heading"], "Invitations"],
|
||||||
ul![class!["invitationsList"], invitations],
|
ul![C!["invitationsList"], invitations],
|
||||||
];
|
];
|
||||||
|
|
||||||
inner_layout(
|
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 {
|
||||||
|
crate::{
|
||||||
use jirs_data::{UserRole, WsMsg};
|
model::{Model, Page},
|
||||||
|
shared::{
|
||||||
use crate::model::{Model, Page};
|
divider,
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
styled_icon::{Icon, StyledIcon},
|
||||||
use crate::shared::{divider, ToNode};
|
ToNode,
|
||||||
use crate::ws::enqueue_ws_msg;
|
},
|
||||||
use crate::{Msg, WebSocketChanged};
|
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>) {
|
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(
|
enqueue_ws_msg(
|
||||||
vec![
|
vec![
|
||||||
WsMsg::UserProjectsLoad,
|
WsMsg::UserProjectsLoad,
|
||||||
@ -20,86 +25,104 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
model.ws.as_ref(),
|
model.ws.as_ref(),
|
||||||
orders,
|
orders,
|
||||||
);
|
);
|
||||||
|
orders.skip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(model: &Model) -> Node<Msg> {
|
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() {
|
let project_info = match model.project.as_ref() {
|
||||||
Some(project) => li![
|
Some(project) => li![
|
||||||
id!["projectInfo"],
|
id!["projectInfo"],
|
||||||
project_icon,
|
project_icon,
|
||||||
div![
|
div![
|
||||||
class!["projectTexts"],
|
C!["projectTexts"],
|
||||||
div![class!["projectName"], project.name.as_str()],
|
div![C!["projectName"], project.name.as_str()],
|
||||||
div![class!["projectCategory"], project.category.to_string()]
|
div![C!["projectCategory"], project.category.to_string()]
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
_ => li![
|
_ => li![
|
||||||
id!["projectInfo"],
|
id!["projectInfo"],
|
||||||
div![
|
div![
|
||||||
class!["projectTexts"],
|
C!["projectTexts"],
|
||||||
div![class!["projectName"], ""],
|
div![C!["projectName"], ""],
|
||||||
div![class!["projectCategory"], ""]
|
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![
|
nav![
|
||||||
id!["sidebar"],
|
id!["sidebar"],
|
||||||
ul![
|
ul![
|
||||||
project_info,
|
project_info,
|
||||||
sidebar_link_item(model, "Kanban Board", Icon::Board, Some(Page::Project)),
|
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> {
|
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 path = page.map(|ref p| p.to_path()).unwrap_or_default();
|
||||||
let mut class_list = vec![];
|
let allow_flag = if page.is_none() {
|
||||||
if page.is_none() {
|
Some(C!["notAllowed"])
|
||||||
class_list.push("notAllowed");
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
if Some(model.page) == page {
|
let active_flag = page.filter(|p| *p == model.page).map(|_| C!["active"]);
|
||||||
class_list.push("active");
|
|
||||||
}
|
|
||||||
let icon_node = StyledIcon::build(icon).build().into_node();
|
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![
|
li![
|
||||||
class!["linkItem"],
|
C!["linkItem"],
|
||||||
class![icon.to_str()],
|
active_flag,
|
||||||
|
allow_flag,
|
||||||
|
C![icon.to_str()],
|
||||||
a![
|
a![
|
||||||
attrs![At::Href => path],
|
attrs![At::Href => path],
|
||||||
|
on_click,
|
||||||
icon_node,
|
icon_node,
|
||||||
div![attrs![At::Class => "linkText"], name],
|
div![C!["linkText"], name],
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
use jirs_data::*;
|
use jirs_data::*;
|
||||||
|
|
||||||
use crate::model::Model;
|
use crate::{
|
||||||
use crate::model::Page;
|
model::{Model, Page},
|
||||||
use crate::Msg;
|
resolve_page, Msg,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod aside;
|
pub mod aside;
|
||||||
pub mod drag;
|
pub mod drag;
|
||||||
@ -37,21 +36,28 @@ pub trait ToChild<'l> {
|
|||||||
fn to_child<'m: 'l>(&'m self) -> Self::Builder;
|
fn to_child<'m: 'l>(&'m self) -> Self::Builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn go_to_board(orders: &mut impl Orders<Msg>) {
|
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));
|
orders.skip().send_msg(Msg::ChangePage(Page::Project));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn go_to_login(orders: &mut impl Orders<Msg>) {
|
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));
|
orders.skip().send_msg(Msg::ChangePage(Page::SignIn));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn go_to(url: &str) {
|
#[inline]
|
||||||
seed::push_route(Url::from_str(url).unwrap());
|
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)
|
model.issues.iter().find(|issue| issue.id == issue_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,14 +66,14 @@ pub trait ToNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn divider() -> Node<Msg> {
|
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> {
|
pub fn inner_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
|
||||||
let modal_node = crate::modal::view(model);
|
let modal_node = crate::modal::view(model);
|
||||||
article![
|
article![
|
||||||
modal_node,
|
modal_node,
|
||||||
class!["inner-layout", "innerPage"],
|
C!["inner-layout", "innerPage"],
|
||||||
id![page_name],
|
id![page_name],
|
||||||
navbar_left::render(model),
|
navbar_left::render(model),
|
||||||
aside::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> {
|
pub fn outer_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
|
||||||
let modal = crate::modal::view(model);
|
let modal = crate::modal::view(model);
|
||||||
article![
|
article![
|
||||||
class!["outer-layout", "outerPage"],
|
C!["outer-layout", "outerPage"],
|
||||||
id![page_name],
|
id![page_name],
|
||||||
modal,
|
modal,
|
||||||
children
|
children
|
||||||
|
@ -2,13 +2,18 @@ use seed::{prelude::*, *};
|
|||||||
|
|
||||||
use jirs_data::{InvitationToken, Message, MessageType, WsMsg};
|
use jirs_data::{InvitationToken, Message, MessageType, WsMsg};
|
||||||
|
|
||||||
use crate::model::Model;
|
use crate::{
|
||||||
use crate::shared::styled_avatar::StyledAvatar;
|
model::Model,
|
||||||
use crate::shared::styled_button::StyledButton;
|
shared::{
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
divider,
|
||||||
use crate::shared::{divider, styled_tooltip, ToNode};
|
styled_avatar::StyledAvatar,
|
||||||
use crate::ws::send_ws_msg;
|
styled_button::StyledButton,
|
||||||
use crate::Msg;
|
styled_icon::{Icon, StyledIcon},
|
||||||
|
styled_tooltip, ToNode,
|
||||||
|
},
|
||||||
|
ws::send_ws_msg,
|
||||||
|
Msg, Page,
|
||||||
|
};
|
||||||
|
|
||||||
trait IntoNavItemIcon {
|
trait IntoNavItemIcon {
|
||||||
fn into_nav_item_icon(self) -> Node<Msg>;
|
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() {
|
let user_icon = match model.user.as_ref() {
|
||||||
Some(user) => i![
|
Some(user) => i![
|
||||||
class!["styledIcon"],
|
C!["styledIcon"],
|
||||||
StyledAvatar::build()
|
StyledAvatar::build()
|
||||||
.size(27)
|
.size(27)
|
||||||
.name(user.name.as_str())
|
.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),
|
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![
|
vec![
|
||||||
about_tooltip_popup(model),
|
about_tooltip_popup(model),
|
||||||
@ -84,14 +95,14 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
|
|||||||
aside![
|
aside![
|
||||||
id!["navbar-left"],
|
id!["navbar-left"],
|
||||||
a![
|
a![
|
||||||
class!["logoLink"],
|
C!["logoLink"],
|
||||||
attrs![At::Href => "/"],
|
attrs![At::Href => "/"],
|
||||||
div![class!["styledLogo"], logo_svg]
|
div![C!["styledLogo"], logo_svg]
|
||||||
],
|
],
|
||||||
issue_nav,
|
issue_nav,
|
||||||
div![
|
div![
|
||||||
class!["bottom"],
|
C!["bottom"],
|
||||||
navbar_left_item("Profile", user_icon, Some("/profile"), None),
|
navbar_left_item("Profile", user_icon, Some("/profile"), Some(go_to_profile)),
|
||||||
messages,
|
messages,
|
||||||
about_tooltip(model, navbar_left_item("About", Icon::Help, None, None)),
|
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>(
|
fn navbar_left_item<I>(
|
||||||
text: &str,
|
text: &str,
|
||||||
icon: I,
|
icon: I,
|
||||||
@ -109,13 +121,12 @@ where
|
|||||||
I: IntoNavItemIcon,
|
I: IntoNavItemIcon,
|
||||||
{
|
{
|
||||||
let styled_icon = icon.into_nav_item_icon();
|
let styled_icon = icon.into_nav_item_icon();
|
||||||
let href = href.unwrap_or_else(|| "#");
|
|
||||||
|
|
||||||
a![
|
a![
|
||||||
class!["item"],
|
C!["item"],
|
||||||
attrs![At::Href => href],
|
attrs![At::Href => href.unwrap_or("#")],
|
||||||
styled_icon,
|
styled_icon,
|
||||||
span![class!["itemText"], text],
|
span![C!["itemText"], text],
|
||||||
on_click,
|
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 |_| {
|
let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| {
|
||||||
Some(Msg::ToggleTooltip(styled_tooltip::Variant::About))
|
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> {
|
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()
|
styled_tooltip::StyledTooltip::build()
|
||||||
.add_class("messagesPopup")
|
.add_class("messagesPopup")
|
||||||
.visible(model.messages_tooltip_visible)
|
.visible(model.messages_tooltip_visible)
|
||||||
@ -166,9 +177,9 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
|
|||||||
} else {
|
} else {
|
||||||
let link_icon = StyledIcon::build(Icon::Link).build().into_node();
|
let link_icon = StyledIcon::build(Icon::Link).build().into_node();
|
||||||
div![
|
div![
|
||||||
class!["hyperlink"],
|
C!["hyperlink"],
|
||||||
a![
|
a![
|
||||||
class!["styledLink"],
|
C!["styledLink"],
|
||||||
attrs![At::Href => hyper_link],
|
attrs![At::Href => hyper_link],
|
||||||
link_icon,
|
link_icon,
|
||||||
hyper_link
|
hyper_link
|
||||||
@ -188,9 +199,9 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
|
|||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
let top = div![
|
let top = div![
|
||||||
class!["top"],
|
C!["top"],
|
||||||
div![class!["summary"], summary],
|
div![C!["summary"], summary],
|
||||||
div![class!["action"], close_button],
|
div![C!["action"], close_button],
|
||||||
];
|
];
|
||||||
|
|
||||||
let node = match message_type {
|
let node = match message_type {
|
||||||
@ -221,23 +232,23 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
|
|||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
div![
|
div![
|
||||||
class!["message"],
|
C!["message"],
|
||||||
attrs![At::Class => format!("{}", message_type)],
|
attrs![At::Class => format!("{}", message_type)],
|
||||||
top,
|
top,
|
||||||
div![class!["description"], message_description],
|
div![C!["description"], message_description],
|
||||||
div![class!["actions"], accept, reject],
|
div![C!["actions"], accept, reject],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
MessageType::AssignedToIssue => div![
|
MessageType::AssignedToIssue => div![
|
||||||
class!["message assignedToIssue"],
|
C!["message assignedToIssue"],
|
||||||
top,
|
top,
|
||||||
div![class!["description"], message_description],
|
div![C!["description"], message_description],
|
||||||
hyperlink,
|
hyperlink,
|
||||||
],
|
],
|
||||||
MessageType::Mention => div![
|
MessageType::Mention => div![
|
||||||
class!["message mention"],
|
C!["message mention"],
|
||||||
top,
|
top,
|
||||||
div![class!["description"], message_description],
|
div![C!["description"], message_description],
|
||||||
hyperlink,
|
hyperlink,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -262,18 +273,18 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
|
|||||||
});
|
});
|
||||||
let body = div![
|
let body = div![
|
||||||
on_click,
|
on_click,
|
||||||
class!["feedbackDropdown"],
|
C!["feedbackDropdown"],
|
||||||
div![
|
div![
|
||||||
class!["feedbackImageCont"],
|
C!["feedbackImageCont"],
|
||||||
img![attrs![At::Src => "/feedback.png"]],
|
img![attrs![At::Src => "/feedback.png"]],
|
||||||
class!["feedbackImage"],
|
C!["feedbackImage"],
|
||||||
],
|
],
|
||||||
div![
|
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."
|
"This simplified Jira clone is built with Seed.rs on the front-end and Actix-Web on the back-end."
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
class!["feedbackParagraph"],
|
C!["feedbackParagraph"],
|
||||||
"Read more on my website or reach out via ",
|
"Read more on my website or reach out via ",
|
||||||
a![
|
a![
|
||||||
attrs![At::Href => "mailto:adrian.wozniak@ita-prog.pl"],
|
attrs![At::Href => "mailto:adrian.wozniak@ita-prog.pl"],
|
||||||
@ -326,7 +337,7 @@ fn parse_description(model: &Model, desc: &str) -> Node<Msg> {
|
|||||||
.size(16)
|
.size(16)
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
span![class!["mention"], avatar, user.name.as_str()]
|
span![C!["mention"], avatar, user.name.as_str()]
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| span![word]);
|
.unwrap_or_else(|| span![word]);
|
||||||
container.add_child(child).add_text(" ");
|
container.add_child(child).add_text(" ");
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use seed::{prelude::*, *};
|
use {
|
||||||
|
crate::{shared::ToNode, Msg},
|
||||||
use crate::shared::ToNode;
|
seed::{prelude::*, *},
|
||||||
use crate::Msg;
|
};
|
||||||
|
|
||||||
pub struct StyledAvatar<'l> {
|
pub struct StyledAvatar<'l> {
|
||||||
avatar_url: Option<&'l str>,
|
avatar_url: Option<&'l str>,
|
||||||
@ -26,6 +26,7 @@ impl<'l> Default for StyledAvatar<'l> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> StyledAvatar<'l> {
|
impl<'l> StyledAvatar<'l> {
|
||||||
|
#[inline(always)]
|
||||||
pub fn build() -> StyledAvatarBuilder<'l> {
|
pub fn build() -> StyledAvatarBuilder<'l> {
|
||||||
StyledAvatarBuilder {
|
StyledAvatarBuilder {
|
||||||
avatar_url: None,
|
avatar_url: None,
|
||||||
@ -39,6 +40,7 @@ impl<'l> StyledAvatar<'l> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> ToNode for StyledAvatar<'l> {
|
impl<'l> ToNode for StyledAvatar<'l> {
|
||||||
|
#[inline(always)]
|
||||||
fn into_node(self) -> Node<Msg> {
|
fn into_node(self) -> Node<Msg> {
|
||||||
render(self)
|
render(self)
|
||||||
}
|
}
|
||||||
@ -54,6 +56,7 @@ pub struct StyledAvatarBuilder<'l> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> StyledAvatarBuilder<'l> {
|
impl<'l> StyledAvatarBuilder<'l> {
|
||||||
|
#[inline(always)]
|
||||||
pub fn avatar_url<'m: 'l>(mut self, avatar_url: &'m str) -> Self {
|
pub fn avatar_url<'m: 'l>(mut self, avatar_url: &'m str) -> Self {
|
||||||
if !avatar_url.is_empty() {
|
if !avatar_url.is_empty() {
|
||||||
self.avatar_url = Some(avatar_url);
|
self.avatar_url = Some(avatar_url);
|
||||||
@ -61,31 +64,37 @@ impl<'l> StyledAvatarBuilder<'l> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn size(mut self, size: u32) -> Self {
|
pub fn size(mut self, size: u32) -> Self {
|
||||||
self.size = Some(size);
|
self.size = Some(size);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn name<'m: 'l>(mut self, name: &'m str) -> Self {
|
pub fn name<'m: 'l>(mut self, name: &'m str) -> Self {
|
||||||
self.name = name;
|
self.name = name;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn on_click(mut self, on_click: EventHandler<Msg>) -> Self {
|
pub fn on_click(mut self, on_click: EventHandler<Msg>) -> Self {
|
||||||
self.on_click = Some(on_click);
|
self.on_click = Some(on_click);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self {
|
pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self {
|
||||||
self.class_list.push(name);
|
self.class_list.push(name);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn user_index(mut self, user_index: usize) -> Self {
|
pub fn user_index(mut self, user_index: usize) -> Self {
|
||||||
self.user_index = user_index;
|
self.user_index = user_index;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn build(self) -> StyledAvatar<'l> {
|
pub fn build(self) -> StyledAvatar<'l> {
|
||||||
StyledAvatar {
|
StyledAvatar {
|
||||||
avatar_url: self.avatar_url,
|
avatar_url: self.avatar_url,
|
||||||
@ -111,11 +120,10 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
|||||||
let index = user_index % 8;
|
let index = user_index % 8;
|
||||||
|
|
||||||
let shared_style = format!("width: {size}px; height: {size}px", size = size);
|
let shared_style = format!("width: {size}px; height: {size}px", size = size);
|
||||||
let handler = match on_click {
|
let class_list: Attrs = {
|
||||||
None => vec![],
|
let s: String = class_list.join(" ");
|
||||||
Some(h) => vec![h],
|
C![s.as_str()]
|
||||||
};
|
};
|
||||||
let class_list: Vec<Attrs> = class_list.into_iter().map(|s| class![s]).collect();
|
|
||||||
let letter = name
|
let letter = name
|
||||||
.chars()
|
.chars()
|
||||||
.rev()
|
.rev()
|
||||||
@ -130,11 +138,10 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
|||||||
url = url
|
url = url
|
||||||
);
|
);
|
||||||
div![
|
div![
|
||||||
class!["styledAvatar"],
|
C!["styledAvatar image"],
|
||||||
class!["image"],
|
|
||||||
class_list,
|
class_list,
|
||||||
attrs![At::Style => style, At::Title => name],
|
attrs![At::Style => style, At::Title => name],
|
||||||
handler
|
on_click
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@ -144,15 +151,14 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
|||||||
size = size
|
size = size
|
||||||
);
|
);
|
||||||
div![
|
div![
|
||||||
class!["styledAvatar"],
|
C!["styledAvatar letter"],
|
||||||
class!["letter"],
|
|
||||||
class_list,
|
class_list,
|
||||||
attrs![
|
attrs![
|
||||||
At::Class => format!("avatarColor{}", index + 1),
|
At::Class => format!("avatarColor{}", index + 1),
|
||||||
At::Style => style
|
At::Style => style
|
||||||
],
|
],
|
||||||
span![letter],
|
span![letter],
|
||||||
handler,
|
on_click,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use seed::{prelude::*, *};
|
use {
|
||||||
|
crate::{shared::ToNode, ButtonId, Msg},
|
||||||
use crate::shared::ToNode;
|
seed::{prelude::*, *},
|
||||||
use crate::{ButtonId, Msg};
|
};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
enum Variant {
|
enum Variant {
|
||||||
@ -45,27 +45,33 @@ pub struct StyledButtonBuilder<'l> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> StyledButtonBuilder<'l> {
|
impl<'l> StyledButtonBuilder<'l> {
|
||||||
|
#[inline(always)]
|
||||||
fn variant(mut self, value: Variant) -> Self {
|
fn variant(mut self, value: Variant) -> Self {
|
||||||
self.variant = Some(value);
|
self.variant = Some(value);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn primary(self) -> Self {
|
pub fn primary(self) -> Self {
|
||||||
self.variant(Variant::Primary)
|
self.variant(Variant::Primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn success(self) -> Self {
|
pub fn success(self) -> Self {
|
||||||
self.variant(Variant::Success)
|
self.variant(Variant::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn danger(self) -> Self {
|
pub fn danger(self) -> Self {
|
||||||
self.variant(Variant::Danger)
|
self.variant(Variant::Danger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn secondary(self) -> Self {
|
pub fn secondary(self) -> Self {
|
||||||
self.variant(Variant::Secondary)
|
self.variant(Variant::Secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn empty(self) -> Self {
|
pub fn empty(self) -> Self {
|
||||||
self.variant(Variant::Empty)
|
self.variant(Variant::Empty)
|
||||||
}
|
}
|
||||||
@ -75,21 +81,25 @@ impl<'l> StyledButtonBuilder<'l> {
|
|||||||
// self
|
// self
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn disabled(mut self, value: bool) -> Self {
|
pub fn disabled(mut self, value: bool) -> Self {
|
||||||
self.disabled = Some(value);
|
self.disabled = Some(value);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn active(mut self, value: bool) -> Self {
|
pub fn active(mut self, value: bool) -> Self {
|
||||||
self.active = Some(value);
|
self.active = Some(value);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn text(mut self, value: &'l str) -> Self {
|
pub fn text(mut self, value: &'l str) -> Self {
|
||||||
self.text = Some(value);
|
self.text = Some(value);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn icon<I>(mut self, value: I) -> Self
|
pub fn icon<I>(mut self, value: I) -> Self
|
||||||
where
|
where
|
||||||
I: ToNode,
|
I: ToNode,
|
||||||
@ -98,37 +108,42 @@ impl<'l> StyledButtonBuilder<'l> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn on_click(mut self, value: EventHandler<Msg>) -> Self {
|
pub fn on_click(mut self, value: EventHandler<Msg>) -> Self {
|
||||||
self.on_click = Some(value);
|
self.on_click = Some(value);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn children(mut self, value: Vec<Node<Msg>>) -> Self {
|
pub fn children(mut self, value: Vec<Node<Msg>>) -> Self {
|
||||||
self.children = Some(value);
|
self.children = Some(value);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn add_class(mut self, name: &'l str) -> Self {
|
pub fn add_class(mut self, name: &'l str) -> Self {
|
||||||
self.class_list.push(name);
|
self.class_list.push(name);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn set_type_reset(mut self) -> Self {
|
pub fn set_type_reset(mut self) -> Self {
|
||||||
self.button_type = Some("reset");
|
self.button_type = Some("reset");
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn build(self) -> StyledButton<'l> {
|
pub fn build(self) -> StyledButton<'l> {
|
||||||
StyledButton {
|
StyledButton {
|
||||||
variant: self.variant.unwrap_or_else(|| Variant::Primary),
|
variant: self.variant.unwrap_or(Variant::Primary),
|
||||||
disabled: self.disabled.unwrap_or_else(|| false),
|
disabled: self.disabled.unwrap_or(false),
|
||||||
active: self.active.unwrap_or_else(|| false),
|
active: self.active.unwrap_or(false),
|
||||||
text: self.text,
|
text: self.text,
|
||||||
icon: self.icon,
|
icon: self.icon,
|
||||||
on_click: self.on_click,
|
on_click: self.on_click,
|
||||||
children: self.children.unwrap_or_default(),
|
children: self.children.unwrap_or_default(),
|
||||||
class_list: self.class_list,
|
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,
|
button_id: self.button_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,17 +163,20 @@ pub struct StyledButton<'l> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> StyledButton<'l> {
|
impl<'l> StyledButton<'l> {
|
||||||
|
#[inline(always)]
|
||||||
pub fn build() -> StyledButtonBuilder<'l> {
|
pub fn build() -> StyledButtonBuilder<'l> {
|
||||||
StyledButtonBuilder::default()
|
StyledButtonBuilder::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> ToNode for StyledButton<'l> {
|
impl<'l> ToNode for StyledButton<'l> {
|
||||||
|
#[inline(always)]
|
||||||
fn into_node(self) -> Node<Msg> {
|
fn into_node(self) -> Node<Msg> {
|
||||||
render(self)
|
render(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn render(values: StyledButton) -> Node<Msg> {
|
pub fn render(values: StyledButton) -> Node<Msg> {
|
||||||
let StyledButton {
|
let StyledButton {
|
||||||
text,
|
text,
|
||||||
@ -187,18 +205,21 @@ pub fn render(values: StyledButton) -> Node<Msg> {
|
|||||||
_ => vec![],
|
_ => 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() {
|
let content = if children.is_empty() && text.is_none() {
|
||||||
empty![]
|
Node::Empty
|
||||||
} else {
|
} 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();
|
let button_id = button_id.map(|id| id.to_str()).unwrap_or_default();
|
||||||
|
|
||||||
seed::button![
|
seed::button![
|
||||||
class!["styledButton"],
|
C!["styledButton"],
|
||||||
class_list,
|
class_list,
|
||||||
attrs![
|
attrs![
|
||||||
At::Id => button_id,
|
At::Id => button_id,
|
||||||
|
@ -189,7 +189,7 @@ fn render(values: StyledCheckbox) -> Node<Msg> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class!["styledCheckbox"],
|
C!["styledCheckbox"],
|
||||||
attrs![At::Class => class_list.join(" ")],
|
attrs![At::Class => class_list.join(" ")],
|
||||||
opt,
|
opt,
|
||||||
]
|
]
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
use seed::{prelude::*, *};
|
use {
|
||||||
|
crate::{
|
||||||
use crate::shared::styled_textarea::StyledTextarea;
|
shared::{styled_textarea::StyledTextarea, ToNode},
|
||||||
use crate::shared::ToNode;
|
FieldChange, FieldId, Msg,
|
||||||
use crate::{FieldChange, FieldId, Msg};
|
},
|
||||||
|
seed::{prelude::*, *},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
|
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
@ -10,15 +12,27 @@ pub enum Mode {
|
|||||||
View,
|
View,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialOrd, PartialEq)]
|
||||||
pub struct StyledEditorState {
|
pub struct StyledEditorState {
|
||||||
pub mode: Mode,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StyledEditor {
|
pub struct StyledEditor {
|
||||||
id: FieldId,
|
id: FieldId,
|
||||||
|
initial_text: String,
|
||||||
text: String,
|
text: String,
|
||||||
|
html: String,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
update_event: Ev,
|
update_event: Ev,
|
||||||
}
|
}
|
||||||
@ -27,7 +41,9 @@ impl StyledEditor {
|
|||||||
pub fn build(id: FieldId) -> StyledEditorBuilder {
|
pub fn build(id: FieldId) -> StyledEditorBuilder {
|
||||||
StyledEditorBuilder {
|
StyledEditorBuilder {
|
||||||
id,
|
id,
|
||||||
text: String::new(),
|
initial_text: "".to_string(),
|
||||||
|
text: "".to_string(),
|
||||||
|
html: "".to_string(),
|
||||||
mode: Mode::View,
|
mode: Mode::View,
|
||||||
update_event: None,
|
update_event: None,
|
||||||
}
|
}
|
||||||
@ -37,7 +53,9 @@ impl StyledEditor {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StyledEditorBuilder {
|
pub struct StyledEditorBuilder {
|
||||||
id: FieldId,
|
id: FieldId,
|
||||||
|
initial_text: String,
|
||||||
text: String,
|
text: String,
|
||||||
|
html: String,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
update_event: Option<Ev>,
|
update_event: Option<Ev>,
|
||||||
}
|
}
|
||||||
@ -51,6 +69,22 @@ impl StyledEditorBuilder {
|
|||||||
self
|
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 {
|
pub fn mode(mut self, mode: Mode) -> Self {
|
||||||
self.mode = mode;
|
self.mode = mode;
|
||||||
self
|
self
|
||||||
@ -59,9 +93,11 @@ impl StyledEditorBuilder {
|
|||||||
pub fn build(self) -> StyledEditor {
|
pub fn build(self) -> StyledEditor {
|
||||||
StyledEditor {
|
StyledEditor {
|
||||||
id: self.id,
|
id: self.id,
|
||||||
|
initial_text: self.initial_text,
|
||||||
text: self.text,
|
text: self.text,
|
||||||
|
html: self.html,
|
||||||
mode: self.mode,
|
mode: self.mode,
|
||||||
update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp),
|
update_event: self.update_event.unwrap_or(Ev::KeyUp),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,22 +116,15 @@ impl ToNode for StyledEditor {
|
|||||||
pub fn render(values: StyledEditor) -> Node<Msg> {
|
pub fn render(values: StyledEditor) -> Node<Msg> {
|
||||||
let StyledEditor {
|
let StyledEditor {
|
||||||
id,
|
id,
|
||||||
text,
|
initial_text,
|
||||||
|
text: _,
|
||||||
|
html,
|
||||||
mode,
|
mode,
|
||||||
update_event,
|
update_event,
|
||||||
} = values;
|
} = values;
|
||||||
|
|
||||||
let field_id = id.clone();
|
let on_editor_clicked = click_handler(id.clone(), Mode::Editor);
|
||||||
let on_editor_clicked = mouse_ev(Ev::Click, move |ev| {
|
let on_view_clicked = click_handler(id.clone(), Mode::View);
|
||||||
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 editor_id = format!("editor-{}", id);
|
||||||
let view_id = format!("view-{}", id);
|
let view_id = format!("view-{}", id);
|
||||||
@ -104,14 +133,12 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
|
|||||||
let text_area = StyledTextarea::build(id)
|
let text_area = StyledTextarea::build(id)
|
||||||
.height(40)
|
.height(40)
|
||||||
.update_on(update_event)
|
.update_on(update_event)
|
||||||
.value(text.as_str())
|
// .disable_auto_resize()
|
||||||
|
.value(initial_text.as_str())
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
let parsed = comrak::markdown_to_html(text.as_str(), &comrak::ComrakOptions::default());
|
let (editor_radio_node, view_radio_node, parsed_node) = match mode {
|
||||||
let parsed_node = Node::from_html(parsed.as_str());
|
|
||||||
|
|
||||||
let (editor_radio_node, view_radio_node) = match mode {
|
|
||||||
Mode::Editor => (
|
Mode::Editor => (
|
||||||
seed::input![
|
seed::input![
|
||||||
id![editor_id.as_str()],
|
id![editor_id.as_str()],
|
||||||
@ -121,28 +148,30 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
|
|||||||
id![view_id.as_str()],
|
id![view_id.as_str()],
|
||||||
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio";],
|
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio";],
|
||||||
],
|
],
|
||||||
|
vec![],
|
||||||
),
|
),
|
||||||
Mode::View => (
|
Mode::View => (
|
||||||
seed::input![
|
seed::input![
|
||||||
id![editor_id.as_str()],
|
id![editor_id.as_str()],
|
||||||
class!["editorRadio"],
|
C!["editorRadio"],
|
||||||
attrs![At::Type => "radio"; At::Name => name.as_str();],
|
attrs![At::Type => "radio"; At::Name => name.as_str();],
|
||||||
],
|
],
|
||||||
seed::input![
|
seed::input![
|
||||||
id![view_id.as_str()],
|
id![view_id.as_str()],
|
||||||
class!["viewRadio"],
|
C!["viewRadio"],
|
||||||
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
|
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
|
||||||
],
|
],
|
||||||
|
Node::from_html(html.as_str()),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
div![
|
div![
|
||||||
attrs![At::Class => "styledEditor"],
|
C!["styledEditor"],
|
||||||
label![
|
label![
|
||||||
if mode == Mode::View {
|
if mode == Mode::View {
|
||||||
class!["navbar viewTab activeTab"]
|
C!["navbar viewTab activeTab"]
|
||||||
} else {
|
} else {
|
||||||
class!["navbar viewTab"]
|
C!["navbar viewTab"]
|
||||||
},
|
},
|
||||||
attrs![At::For => view_id.as_str()],
|
attrs![At::For => view_id.as_str()],
|
||||||
"View",
|
"View",
|
||||||
@ -161,6 +190,14 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
|
|||||||
editor_radio_node,
|
editor_radio_node,
|
||||||
text_area,
|
text_area,
|
||||||
view_radio_node,
|
view_radio_node,
|
||||||
div![attrs![At::Class => "view"], parsed_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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -74,10 +74,6 @@ pub fn render(values: StyledForm) -> Node<Msg> {
|
|||||||
seed::form![
|
seed::form![
|
||||||
handlers,
|
handlers,
|
||||||
attrs![At::Class => "styledForm"],
|
attrs![At::Class => "styledForm"],
|
||||||
div![
|
div![C!["formElement"], div![C!["formHeading"], heading], fields],
|
||||||
class!["formElement"],
|
|
||||||
div![class!["formHeading"], heading],
|
|
||||||
fields
|
|
||||||
],
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -346,30 +346,33 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(Option::is_some)
|
.filter_map(|o| o)
|
||||||
.map(|o| o.unwrap())
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let class_list: Vec<seed::Attrs> = class_list
|
let class_list: Vec<seed::Attrs> = class_list
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| match s {
|
.map(|s| match s {
|
||||||
Cow::Borrowed(s) => class![s],
|
Cow::Borrowed(s) => C![s],
|
||||||
Cow::Owned(s) => class![s.as_str()],
|
Cow::Owned(s) => C![s.as_str()],
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let style_list = style_list
|
let style_list = style_list.into_iter().fold("".to_string(), |mut mem, s| {
|
||||||
.iter()
|
match s {
|
||||||
.map(|s| match s {
|
Cow::Borrowed(s) => {
|
||||||
Cow::Borrowed(s) => s,
|
mem.push_str(s);
|
||||||
Cow::Owned(s) => s.as_str(),
|
}
|
||||||
})
|
Cow::Owned(owned) => {
|
||||||
.collect::<Vec<&str>>()
|
mem.push_str(owned.as_str());
|
||||||
.join(";");
|
}
|
||||||
|
}
|
||||||
|
mem.push(';');
|
||||||
|
mem
|
||||||
|
});
|
||||||
|
|
||||||
i![
|
i![
|
||||||
class!["styledIcon"],
|
C!["styledIcon"],
|
||||||
class_list,
|
class_list,
|
||||||
class![icon.to_str()],
|
C![icon.to_str()],
|
||||||
styles,
|
styles,
|
||||||
attrs![ At::Style => style_list ],
|
attrs![ At::Style => style_list ],
|
||||||
on_click,
|
on_click,
|
||||||
|
@ -104,19 +104,15 @@ fn render(values: StyledImageInput) -> Node<Msg> {
|
|||||||
let input_id = id.to_string();
|
let input_id = id.to_string();
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class!["styledImageInput"],
|
C!["styledImageInput"],
|
||||||
attrs![At::Class => class_list.join(" ")],
|
attrs![At::Class => class_list.join(" ")],
|
||||||
label![
|
label![
|
||||||
class!["label"],
|
C!["label"],
|
||||||
attrs![At::For => input_id],
|
attrs![At::For => input_id],
|
||||||
img![
|
img![C!["mask"], attrs![At::Src => url.unwrap_or_default()], " "]
|
||||||
class!["mask"],
|
|
||||||
attrs![At::Src => url.unwrap_or_default()],
|
|
||||||
" "
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
input![
|
input![
|
||||||
class!["input"],
|
C!["input"],
|
||||||
attrs![At::Type => "file", At::Id => input_id],
|
attrs![At::Type => "file", At::Id => input_id],
|
||||||
on_change
|
on_change
|
||||||
]
|
]
|
||||||
|
@ -223,18 +223,14 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
|||||||
Msg::StrInputChanged(field_id, value)
|
Msg::StrInputChanged(field_id, value)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let on_keyup = {
|
let on_keyup = ev(Ev::KeyUp, move |event| {
|
||||||
ev(Ev::KeyUp, move |event| {
|
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
None as Option<Msg>
|
None as Option<Msg>
|
||||||
})
|
});
|
||||||
};
|
let on_click = ev(Ev::Click, move |event| {
|
||||||
let on_click = {
|
|
||||||
ev(Ev::Click, move |event| {
|
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
None as Option<Msg>
|
None as Option<Msg>
|
||||||
})
|
});
|
||||||
};
|
|
||||||
|
|
||||||
div![
|
div![
|
||||||
C!["styledInput"],
|
C!["styledInput"],
|
||||||
@ -251,7 +247,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
|||||||
At::Id => format!("{}", id),
|
At::Id => format!("{}", id),
|
||||||
At::Class => input_class_list.join(" "),
|
At::Class => input_class_list.join(" "),
|
||||||
At::Value => value.unwrap_or_default(),
|
At::Value => value.unwrap_or_default(),
|
||||||
At::Type => input_type.unwrap_or_else(|| "text"),
|
At::Type => input_type.unwrap_or("text"),
|
||||||
|
|
||||||
],
|
],
|
||||||
if auto_focus {
|
if auto_focus {
|
||||||
|
@ -65,7 +65,7 @@ pub fn render(values: StyledLink) -> Node<Msg> {
|
|||||||
} = values;
|
} = values;
|
||||||
|
|
||||||
a![
|
a![
|
||||||
class!["styledLink"],
|
C!["styledLink"],
|
||||||
attrs![
|
attrs![
|
||||||
At::Class => class_list.join(" "),
|
At::Class => class_list.join(" "),
|
||||||
At::Href => href,
|
At::Href => href,
|
||||||
|
@ -116,9 +116,14 @@ pub fn render(values: StyledModal) -> Node<Msg> {
|
|||||||
empty![]
|
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| {
|
let body_handler = mouse_ev(Ev::Click, |ev| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
|
ev.prevent_default();
|
||||||
None as Option<Msg>
|
None as Option<Msg>
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -682,7 +682,7 @@ fn first_row(click_handler: EventHandler<Msg>) -> Node<Msg> {
|
|||||||
click_handler.clone(),
|
click_handler.clone(),
|
||||||
);
|
);
|
||||||
div![
|
div![
|
||||||
class!["group justify"],
|
C!["group justify"],
|
||||||
justify_all_button,
|
justify_all_button,
|
||||||
justify_center_button,
|
justify_center_button,
|
||||||
justify_left_button,
|
justify_left_button,
|
||||||
@ -723,7 +723,7 @@ fn first_row(click_handler: EventHandler<Msg>) -> Node<Msg> {
|
|||||||
}),
|
}),
|
||||||
);*/
|
);*/
|
||||||
div![
|
div![
|
||||||
class!["group system"],
|
C!["group system"],
|
||||||
// clip_board_button,
|
// clip_board_button,
|
||||||
// copy_button,
|
// copy_button,
|
||||||
// cut_button,
|
// cut_button,
|
||||||
@ -777,7 +777,7 @@ fn first_row(click_handler: EventHandler<Msg>) -> Node<Msg> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class!["group formatting"],
|
C!["group formatting"],
|
||||||
bold_button,
|
bold_button,
|
||||||
italic_button,
|
italic_button,
|
||||||
underline_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(
|
fn second_row(
|
||||||
@ -825,7 +825,7 @@ fn second_row(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
div![
|
div![
|
||||||
class!["group align"],
|
C!["group align"],
|
||||||
align_center_button,
|
align_center_button,
|
||||||
align_left_button,
|
align_left_button,
|
||||||
align_right_button,
|
align_right_button,
|
||||||
@ -848,10 +848,10 @@ fn second_row(
|
|||||||
.empty()
|
.empty()
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
span![class!["headingOption"], button]
|
span![C!["headingOption"], button]
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let heading_button = span![class!["headingList"], options];
|
let heading_button = span![C!["headingList"], options];
|
||||||
|
|
||||||
/*let _field_id = values.field_id.clone();
|
/*let _field_id = values.field_id.clone();
|
||||||
let _small_cap_button = styled_rte_button(
|
let _small_cap_button = styled_rte_button(
|
||||||
@ -872,7 +872,7 @@ fn second_row(
|
|||||||
}),
|
}),
|
||||||
);*/
|
);*/
|
||||||
div![
|
div![
|
||||||
class!["group font"],
|
C!["group font"],
|
||||||
// font_button,
|
// font_button,
|
||||||
heading_button,
|
heading_button,
|
||||||
// small_cap_button,
|
// small_cap_button,
|
||||||
@ -924,7 +924,7 @@ fn second_row(
|
|||||||
code_alt_button.add_child(code_tooltip(values, click_handler.clone()));
|
code_alt_button.add_child(code_tooltip(values, click_handler.clone()));
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class!["group insert"],
|
C!["group insert"],
|
||||||
paragraph_button,
|
paragraph_button,
|
||||||
table_button,
|
table_button,
|
||||||
code_alt_button,
|
code_alt_button,
|
||||||
@ -943,11 +943,11 @@ fn second_row(
|
|||||||
);
|
);
|
||||||
let outdent_button =
|
let outdent_button =
|
||||||
styled_rte_button("Outdent", ButtonId::Outdent, Icon::Outdent, click_handler);
|
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![
|
div![
|
||||||
class!["row secondRow"],
|
C!["row secondRow"],
|
||||||
font_group,
|
font_group,
|
||||||
// align_group,
|
// align_group,
|
||||||
insert_group,
|
insert_group,
|
||||||
@ -1003,12 +1003,12 @@ fn table_tooltip(
|
|||||||
.table_tooltip()
|
.table_tooltip()
|
||||||
.visible(visible)
|
.visible(visible)
|
||||||
.add_child(h2![span!["Add table"], close_table_tooltip])
|
.add_child(h2![span!["Add table"], close_table_tooltip])
|
||||||
.add_child(div![class!["inputs"], span!["Rows"], seed::input![
|
.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],
|
attrs![At::Type => "range"; At::Step => "1"; At::Min => "1"; At::Max => "10"; At::Value => rows],
|
||||||
on_rows_change
|
on_rows_change
|
||||||
]])
|
]])
|
||||||
.add_child(div![
|
.add_child(div![
|
||||||
class!["inputs"],
|
C!["inputs"],
|
||||||
span!["Columns"],
|
span!["Columns"],
|
||||||
seed::input![
|
seed::input![
|
||||||
attrs![At::Type => "range"; At::Step => "1"; At::Min => "1"; At::Max => "10"; At::Value => cols],
|
attrs![At::Type => "range"; At::Step => "1"; At::Min => "1"; At::Max => "10"; At::Value => cols],
|
||||||
@ -1025,7 +1025,7 @@ fn table_tooltip(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
seed::div![
|
seed::div![
|
||||||
class!["tablePreview"],
|
C!["tablePreview"],
|
||||||
seed::table![tbody![body]],
|
seed::table![tbody![body]],
|
||||||
input![attrs![At::Type => "button"; At::Id => "rteInsertTable"; At::Value => "Insert"], on_submit],
|
input![attrs![At::Type => "button"; At::Id => "rteInsertTable"; At::Value => "Insert"], on_submit],
|
||||||
]
|
]
|
||||||
@ -1121,9 +1121,5 @@ fn styled_rte_button(
|
|||||||
.empty()
|
.empty()
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
span![
|
span![C!["styledRteButton"], attrs![At::Title => title], button]
|
||||||
class!["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 => name.as_deref().map(|s| format!("{}Label", s)).unwrap_or_default(),
|
||||||
At::Class => class_list.join(" "),
|
At::Class => class_list.join(" "),
|
||||||
],
|
],
|
||||||
class![label_class.as_str()],
|
C![label_class.as_str()],
|
||||||
text
|
text
|
||||||
],
|
],
|
||||||
_ => empty![],
|
_ => empty![],
|
||||||
};
|
};
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class![variant.to_str()],
|
C![variant.to_str()],
|
||||||
class![wrapper_class.as_str()],
|
C![wrapper_class.as_str()],
|
||||||
attrs![At::Class => class_list.join(" ")],
|
attrs![At::Class => class_list.join(" ")],
|
||||||
icon_node,
|
icon_node,
|
||||||
label_node
|
label_node
|
||||||
|
@ -98,7 +98,7 @@ impl<'l> StyledTextareaBuilder<'l> {
|
|||||||
height: self.height.unwrap_or(110),
|
height: self.height.unwrap_or(110),
|
||||||
class_list: self.class_list,
|
class_list: self.class_list,
|
||||||
max_height: self.max_height.unwrap_or_default(),
|
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,
|
placeholder: self.placeholder,
|
||||||
disable_auto_resize: self.disable_auto_resize,
|
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| {
|
let text_input_handler = ev(update_event, move |event| {
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
|
|
||||||
let target = event.target().unwrap();
|
let value = event
|
||||||
let textarea = seed::to_textarea(&target);
|
.target()
|
||||||
let value = textarea.value();
|
.map(|target| seed::to_textarea(&target).value())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
if handler_disable_auto_resize && value.contains('\n') {
|
if handler_disable_auto_resize && value.contains('\n') {
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::StrInputChanged(
|
Some(Msg::StrInputChanged(
|
||||||
id,
|
id,
|
||||||
if handler_disable_auto_resize {
|
if handler_disable_auto_resize {
|
||||||
value.trim().to_string()
|
value.trim().to_string()
|
||||||
} else {
|
} else {
|
||||||
value
|
value
|
||||||
},
|
},
|
||||||
)
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
class_list.push("textAreaInput");
|
class_list.push("textAreaInput");
|
||||||
|
|
||||||
div![
|
div![
|
||||||
attrs![At::Class => "styledTextArea"],
|
C!["styledTextArea"],
|
||||||
div![attrs![At::Class => "textAreaHeading"]],
|
div![C!["textAreaHeading"]],
|
||||||
textarea![
|
textarea![
|
||||||
attrs![
|
attrs![
|
||||||
At::Class => class_list.join(" ");
|
At::Class => class_list.join(" ");
|
||||||
At::AutoFocus => "true";
|
At::AutoFocus => "true";
|
||||||
At::Style => style_list.join(";");
|
At::Style => style_list.join(";");
|
||||||
At::Placeholder => placeholder.unwrap_or_default();
|
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,
|
value,
|
||||||
resize_handler,
|
resize_handler,
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
use seed::{prelude::*, *};
|
use {
|
||||||
|
crate::{
|
||||||
use jirs_data::{TimeTracking, UpdateIssuePayload};
|
modal::time_tracking::value_for_time_tracking,
|
||||||
|
modals::issues_edit::Model as EditIssueModal,
|
||||||
use crate::modal::time_tracking::value_for_time_tracking;
|
model::{ModalType, Model},
|
||||||
use crate::model::{EditIssueModal, ModalType, Model};
|
shared::{
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
styled_icon::{Icon, StyledIcon},
|
||||||
use crate::shared::ToNode;
|
ToNode,
|
||||||
use crate::Msg;
|
},
|
||||||
|
Msg,
|
||||||
|
},
|
||||||
|
jirs_data::{TimeTracking, UpdateIssuePayload},
|
||||||
|
seed::{prelude::*, *},
|
||||||
|
};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn fibonacci_values() -> Vec<u32> {
|
pub fn fibonacci_values() -> Vec<u32> {
|
||||||
vec![0, 1, 2, 3, 5, 8, 13, 21, 34, 55]
|
vec![0, 1, 2, 3, 5, 8, 13, 21, 34, 55]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tracking_link(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
pub fn tracking_link(model: &Model, modal: &crate::modals::issues_edit::Model) -> Node<Msg> {
|
||||||
let EditIssueModal { id, .. } = modal;
|
let crate::modals::issues_edit::Model { id, .. } = modal;
|
||||||
|
|
||||||
let issue_id = *id;
|
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)))
|
Msg::ModalOpened(Box::new(ModalType::TimeTracking(issue_id)))
|
||||||
});
|
});
|
||||||
|
|
||||||
div![
|
div![C!["trackingLink"], handler, tracking_widget(model, modal),]
|
||||||
class!["trackingLink"],
|
|
||||||
handler,
|
|
||||||
tracking_widget(model, modal),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
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);
|
let remaining_node: Node<Msg> = remaining_node(time_remaining, estimate, time_tracking_type);
|
||||||
|
|
||||||
div![
|
div![
|
||||||
class!["trackingWidget"],
|
C!["trackingWidget"],
|
||||||
icon,
|
icon,
|
||||||
div![
|
div![
|
||||||
class!["right"],
|
C!["right"],
|
||||||
div![
|
div![
|
||||||
class!["barCounter"],
|
C!["barCounter"],
|
||||||
div![
|
div![
|
||||||
class!["bar"],
|
C!["bar"],
|
||||||
attrs![At::Style => format!("width: {}%", bar_width)]
|
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![
|
vec![
|
||||||
WsMsg::IssueStatusesLoad,
|
WsMsg::IssueStatusesLoad,
|
||||||
WsMsg::ProjectIssuesLoad,
|
WsMsg::ProjectIssuesLoad,
|
||||||
|
WsMsg::ProjectUsersLoad,
|
||||||
WsMsg::EpicsLoad,
|
WsMsg::EpicsLoad,
|
||||||
],
|
],
|
||||||
model.ws.as_ref(),
|
model.ws.as_ref(),
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
use seed::prelude::*;
|
use seed::prelude::*;
|
||||||
|
|
||||||
pub use init_load_sets::*;
|
pub use init_load_sets::*;
|
||||||
use jirs_data::WsMsg;
|
use jirs_data::*;
|
||||||
|
|
||||||
use crate::model::*;
|
use crate::{
|
||||||
use crate::shared::{go_to_board, write_auth_token};
|
model::*,
|
||||||
use crate::{Msg, WebSocketChanged};
|
shared::{go_to_board, write_auth_token},
|
||||||
|
Msg, OperationKind, ResourceKind, WebSocketChanged,
|
||||||
|
};
|
||||||
|
|
||||||
mod init_load_sets;
|
mod init_load_sets;
|
||||||
|
|
||||||
@ -88,36 +90,60 @@ pub async fn read_incoming(msg: WebSocketMessage) -> Msg {
|
|||||||
Msg::WebSocketChange(WebSocketChanged::WebSocketMessageLoaded(bytes))
|
Msg::WebSocketChange(WebSocketChanged::WebSocketMessageLoaded(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
// auth
|
// auth
|
||||||
WsMsg::AuthorizeLoaded(Ok(user)) => {
|
WsMsg::AuthorizeLoaded(Ok(user)) => {
|
||||||
model.user = Some(user.clone());
|
model.user = Some(user);
|
||||||
if is_non_logged_area() {
|
if is_non_logged_area() {
|
||||||
go_to_board(orders);
|
go_to_board(orders);
|
||||||
}
|
}
|
||||||
orders
|
orders
|
||||||
.skip()
|
.skip()
|
||||||
.send_msg(Msg::UserChanged(model.user.as_ref().cloned()));
|
.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 => {
|
WsMsg::AuthorizeExpired => {
|
||||||
use seed::*;
|
use seed::*;
|
||||||
|
|
||||||
log!("Received token expired");
|
log!("Received token expired");
|
||||||
if let Ok(msg) = write_auth_token(None) {
|
if let Ok(msg) = write_auth_token(None) {
|
||||||
orders.skip().send_msg(msg);
|
orders.skip().send_msg(msg).send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::Auth,
|
||||||
|
OperationKind::SingleRemoved,
|
||||||
|
model.user.as_ref().map(|u| u.id),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// project
|
// project
|
||||||
WsMsg::ProjectsLoaded(v) => {
|
WsMsg::ProjectsLoaded(v) => {
|
||||||
model.projects = v.clone();
|
model.projects = v;
|
||||||
init_current_project(model, orders);
|
init_current_project(model, orders);
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::Project,
|
||||||
|
OperationKind::ListLoaded,
|
||||||
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// user projects
|
// user projects
|
||||||
WsMsg::UserProjectsLoaded(v) => {
|
WsMsg::UserProjectsLoaded(v) => {
|
||||||
model.user_projects = v.clone();
|
|
||||||
model.current_user_project = v.iter().find(|up| up.is_current).cloned();
|
model.current_user_project = v.iter().find(|up| up.is_current).cloned();
|
||||||
|
model.user_projects = v;
|
||||||
init_current_project(model, orders);
|
init_current_project(model, orders);
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::UserProject,
|
||||||
|
OperationKind::ListLoaded,
|
||||||
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
WsMsg::UserProjectCurrentChanged(user_project) => {
|
WsMsg::UserProjectCurrentChanged(user_project) => {
|
||||||
let mut old = vec![];
|
let mut old = vec![];
|
||||||
@ -126,60 +152,102 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
up.is_current = up.id == user_project.id;
|
up.is_current = up.id == user_project.id;
|
||||||
model.user_projects.push(up);
|
model.user_projects.push(up);
|
||||||
}
|
}
|
||||||
model.current_user_project = Some(user_project.clone());
|
model.current_user_project = Some(user_project);
|
||||||
init_current_project(model, orders);
|
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
|
// issues
|
||||||
WsMsg::ProjectIssuesLoaded(v) => {
|
WsMsg::ProjectIssuesLoaded(mut v) => {
|
||||||
let mut v = v.clone();
|
|
||||||
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
|
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
|
||||||
model.issues = v;
|
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
|
// issue statuses
|
||||||
WsMsg::IssueStatusesLoaded(v) => {
|
WsMsg::IssueStatusesLoaded(v) => {
|
||||||
model.issue_statuses = v.clone();
|
model.issue_statuses = v;
|
||||||
model
|
model
|
||||||
.issue_statuses
|
.issue_statuses
|
||||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::IssueStatus,
|
||||||
|
OperationKind::ListLoaded,
|
||||||
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
WsMsg::IssueStatusCreated(is) => {
|
WsMsg::IssueStatusCreated(is) => {
|
||||||
model.issue_statuses.push(is.clone());
|
let id = is.id;
|
||||||
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.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
|
model
|
||||||
.issue_statuses
|
.issue_statuses
|
||||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
.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) => {
|
WsMsg::IssueStatusDeleted(dropped_id, _count) => {
|
||||||
let mut old = vec![];
|
let mut old = vec![];
|
||||||
std::mem::swap(&mut model.issue_statuses, &mut old);
|
std::mem::swap(&mut model.issue_statuses, &mut old);
|
||||||
for is in old {
|
for is in old {
|
||||||
if is.id != *dropped_id {
|
if is.id != dropped_id {
|
||||||
model.issue_statuses.push(is);
|
model.issue_statuses.push(is);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model
|
model
|
||||||
.issue_statuses
|
.issue_statuses
|
||||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
.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) => {
|
WsMsg::IssueDeleted(id, _count) => {
|
||||||
let mut old = vec![];
|
let mut old = vec![];
|
||||||
std::mem::swap(&mut model.issue_statuses, &mut old);
|
std::mem::swap(&mut model.issue_statuses, &mut old);
|
||||||
for is in old {
|
for is in old {
|
||||||
if is.id == *id {
|
if is.id == id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
model.issue_statuses.push(is);
|
model.issue_statuses.push(is);
|
||||||
@ -187,13 +255,26 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
model
|
model
|
||||||
.issue_statuses
|
.issue_statuses
|
||||||
.sort_by(|a, b| a.position.cmp(&b.position));
|
.sort_by(|a, b| a.position.cmp(&b.position));
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::Issue,
|
||||||
|
OperationKind::SingleRemoved,
|
||||||
|
Some(id),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// users
|
// users
|
||||||
WsMsg::ProjectUsersLoaded(v) => {
|
WsMsg::ProjectUsersLoaded(v) => {
|
||||||
model.users = v.clone();
|
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
|
// comments
|
||||||
WsMsg::IssueCommentsLoaded(comments) => {
|
WsMsg::IssueCommentsLoaded(mut comments) => {
|
||||||
let issue_id = match model.modals.get(0) {
|
let issue_id = match model.modals.get(0) {
|
||||||
Some(ModalType::EditIssue(issue_id, _)) => *issue_id,
|
Some(ModalType::EditIssue(issue_id, _)) => *issue_id,
|
||||||
_ => return,
|
_ => return,
|
||||||
@ -201,99 +282,124 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
if comments.iter().any(|c| c.issue_id != issue_id) {
|
if comments.iter().any(|c| c.issue_id != issue_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut v = comments.clone();
|
comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
|
||||||
v.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
|
model.comments = comments;
|
||||||
model.comments = v;
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
}
|
ResourceKind::Comment,
|
||||||
WsMsg::CommentUpdated(comment) => {
|
OperationKind::ListLoaded,
|
||||||
let mut old = vec![];
|
None,
|
||||||
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::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) => {
|
WsMsg::CommentDeleted(comment_id, _count) => {
|
||||||
let mut old = vec![];
|
if let Some(idx) = model.comments.iter().position(|c| c.id == comment_id) {
|
||||||
std::mem::swap(&mut model.comments, &mut old);
|
model.comments.remove(idx);
|
||||||
for comment in old.into_iter() {
|
|
||||||
if *comment_id != comment.id {
|
|
||||||
model.comments.push(comment);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::Comment,
|
||||||
|
OperationKind::SingleRemoved,
|
||||||
|
Some(comment_id),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
WsMsg::AvatarUrlChanged(user_id, avatar_url) => {
|
WsMsg::AvatarUrlChanged(user_id, avatar_url) => {
|
||||||
for user in model.users.iter_mut() {
|
for user in model.users.iter_mut() {
|
||||||
if user.id == *user_id {
|
if user.id == user_id {
|
||||||
user.avatar_url = Some(avatar_url.clone());
|
user.avatar_url = Some(avatar_url.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(me) = model.user.as_mut() {
|
if let Some(me) = model.user.as_mut() {
|
||||||
if me.id == *user_id {
|
if me.id == user_id {
|
||||||
me.avatar_url = Some(avatar_url.clone());
|
me.avatar_url = Some(avatar_url.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::User,
|
||||||
|
OperationKind::SingleModified,
|
||||||
|
Some(user_id),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// messages
|
// messages
|
||||||
WsMsg::Message(received) => {
|
WsMsg::MessageUpdated(mut received) => {
|
||||||
let mut old = vec![];
|
if let Some(idx) = model.messages.iter().position(|m| m.id == received.id) {
|
||||||
std::mem::swap(&mut old, &mut model.messages);
|
std::mem::swap(&mut model.messages[idx], &mut received);
|
||||||
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));
|
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) => {
|
WsMsg::MessagesLoaded(v) => {
|
||||||
model.messages = v.clone();
|
model.messages = v;
|
||||||
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
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) => {
|
WsMsg::MessageMarkedSeen(id, _count) => {
|
||||||
let mut old = vec![];
|
if let Some(idx) = model.messages.iter().position(|m| m.id == id) {
|
||||||
std::mem::swap(&mut old, &mut model.messages);
|
model.messages.remove(idx);
|
||||||
for m in old {
|
|
||||||
if m.id != *id {
|
|
||||||
model.messages.push(m);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::Message,
|
||||||
|
OperationKind::SingleRemoved,
|
||||||
|
Some(id),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// epics
|
// epics
|
||||||
WsMsg::EpicsLoaded(epics) => {
|
WsMsg::EpicsLoaded(epics) => {
|
||||||
model.epics = epics.clone();
|
model.epics = epics;
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::Epic,
|
||||||
|
OperationKind::ListLoaded,
|
||||||
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
WsMsg::EpicCreated(epic) => {
|
WsMsg::EpicCreated(epic) => {
|
||||||
model.epics.push(epic.clone());
|
let id = epic.id;
|
||||||
|
model.epics.push(epic);
|
||||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::Epic,
|
||||||
|
OperationKind::SingleCreated,
|
||||||
|
Some(id),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
WsMsg::EpicUpdated(epic) => {
|
WsMsg::EpicUpdated(mut epic) => {
|
||||||
let mut old = vec![];
|
if let Some(idx) = model.epics.iter().position(|e| e.id == epic.id) {
|
||||||
std::mem::swap(&mut old, &mut model.epics);
|
std::mem::swap(&mut model.epics[idx], &mut epic);
|
||||||
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));
|
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) => {
|
WsMsg::EpicDeleted(id, _count) => {
|
||||||
let mut old = vec![];
|
if let Some(idx) = model.epics.iter().position(|e| e.id == id) {
|
||||||
std::mem::swap(&mut old, &mut model.epics);
|
model.epics.remove(idx);
|
||||||
for current in old {
|
|
||||||
if current.id != *id {
|
|
||||||
model.epics.push(current);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
orders.send_msg(Msg::ResourceChanged(
|
||||||
|
ResourceKind::Epic,
|
||||||
|
OperationKind::SingleRemoved,
|
||||||
|
Some(id),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
@ -317,8 +423,5 @@ fn init_current_project(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
|
|
||||||
fn is_non_logged_area() -> bool {
|
fn is_non_logged_area() -> bool {
|
||||||
let pathname = seed::document().location().unwrap().pathname().unwrap();
|
let pathname = seed::document().location().unwrap().pathname().unwrap();
|
||||||
match pathname.as_str() {
|
matches!(pathname.as_str(), "/login" | "/register" | "/invite")
|
||||||
"/login" | "/register" | "/invite" => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@ const wsUrl = () => `${getProtocol()}//${getWsHostName()}:${process.env.JIRS_SER
|
|||||||
import("/jirs.js").then(async module => {
|
import("/jirs.js").then(async module => {
|
||||||
// window.module = module;
|
// window.module = module;
|
||||||
await module.default();
|
await module.default();
|
||||||
const host_url = `${location.protocol}//${process.env.JIRS_SERVER_BIND}:${process.env.JIRS_SERVER_PORT}`;
|
const host_url = `${ location.protocol }//${ process.env.JIRS_SERVER_BIND }:${ process.env.JIRS_SERVER_PORT }`;
|
||||||
module.render(host_url, wsUrl());
|
module.render(host_url, wsUrl());
|
||||||
document.querySelector('main').className = '';
|
document.querySelector('main').className = '';
|
||||||
document.querySelector('.spinner').remove();
|
const spinner = document.querySelector('.spinner');
|
||||||
|
spinner && spinner.remove();
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user