Upgrade and fix slow autoresize

Fix global handler

Fix initial content, fix settings status, restore partially working rte

Fmt

Implement keyup for rte

Save editor type, switch in mixed mode

Fix initial state for switch between editors

New logger, add handle multiple editors

Nicer editor switcher, fix change user mode in project settings

Epics starts and ends at. Fix DateTime Input month name
This commit is contained in:
Adrian Woźniak 2021-04-25 11:31:52 +02:00 committed by eraden
parent 9994ab8165
commit 3ee08dea54
67 changed files with 2534 additions and 1490 deletions

237
Cargo.lock generated
View File

@ -15,7 +15,7 @@ dependencies = [
"bytes 0.5.6", "bytes 0.5.6",
"crossbeam-channel", "crossbeam-channel",
"derive_more", "derive_more",
"futures 0.3.14", "futures",
"lazy_static", "lazy_static",
"log", "log",
"parking_lot 0.10.2", "parking_lot 0.10.2",
@ -91,11 +91,11 @@ checksum = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c"
dependencies = [ dependencies = [
"actix-codec 0.2.0", "actix-codec 0.2.0",
"actix-rt", "actix-rt",
"actix-service", "actix-service 1.0.6",
"actix-utils 1.0.6", "actix-utils 1.0.6",
"derive_more", "derive_more",
"either", "either",
"futures 0.3.14", "futures",
"http", "http",
"log", "log",
"trust-dns-proto 0.18.0-alpha.2", "trust-dns-proto 0.18.0-alpha.2",
@ -110,7 +110,7 @@ checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc"
dependencies = [ dependencies = [
"actix-codec 0.3.0", "actix-codec 0.3.0",
"actix-rt", "actix-rt",
"actix-service", "actix-service 1.0.6",
"actix-utils 2.0.0", "actix-utils 2.0.0",
"derive_more", "derive_more",
"either", "either",
@ -141,7 +141,7 @@ 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 = "c51e8a9146c12fce92a6e4c24b8c4d9b05268130bfd8d61bc587e822c32ce689" checksum = "c51e8a9146c12fce92a6e4c24b8c4d9b05268130bfd8d61bc587e822c32ce689"
dependencies = [ dependencies = [
"actix-service", "actix-service 1.0.6",
"actix-web", "actix-web",
"bitflags", "bitflags",
"bytes 0.5.6", "bytes 0.5.6",
@ -164,7 +164,7 @@ dependencies = [
"actix-codec 0.2.0", "actix-codec 0.2.0",
"actix-connect 1.0.2", "actix-connect 1.0.2",
"actix-rt", "actix-rt",
"actix-service", "actix-service 1.0.6",
"actix-threadpool", "actix-threadpool",
"actix-utils 1.0.6", "actix-utils 1.0.6",
"base64 0.11.0", "base64 0.11.0",
@ -208,7 +208,7 @@ dependencies = [
"actix-codec 0.3.0", "actix-codec 0.3.0",
"actix-connect 2.0.0", "actix-connect 2.0.0",
"actix-rt", "actix-rt",
"actix-service", "actix-service 1.0.6",
"actix-threadpool", "actix-threadpool",
"actix-utils 2.0.0", "actix-utils 2.0.0",
"base64 0.13.0", "base64 0.13.0",
@ -235,7 +235,7 @@ dependencies = [
"log", "log",
"mime", "mime",
"percent-encoding", "percent-encoding",
"pin-project 1.0.6", "pin-project 1.0.7",
"rand 0.7.3", "rand 0.7.3",
"regex", "regex",
"serde", "serde",
@ -262,7 +262,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "774bfeb11b54bf9c857a005b8ab893293da4eaff79261a66a9200dab7f5ab6e3" checksum = "774bfeb11b54bf9c857a005b8ab893293da4eaff79261a66a9200dab7f5ab6e3"
dependencies = [ dependencies = [
"actix-service", "actix-service 1.0.6",
"actix-utils 2.0.0", "actix-utils 2.0.0",
"actix-web", "actix-web",
"bytes 0.5.6", "bytes 0.5.6",
@ -310,7 +310,7 @@ checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e"
dependencies = [ dependencies = [
"actix-codec 0.3.0", "actix-codec 0.3.0",
"actix-rt", "actix-rt",
"actix-service", "actix-service 1.0.6",
"actix-utils 2.0.0", "actix-utils 2.0.0",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -332,6 +332,17 @@ dependencies = [
"pin-project 0.4.28", "pin-project 0.4.28",
] ]
[[package]]
name = "actix-service"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77f5f9d66a8730d0fae62c26f3424f5751e5518086628a40b7ab6fca4a705034"
dependencies = [
"futures-core",
"paste",
"pin-project-lite 0.2.6",
]
[[package]] [[package]]
name = "actix-testing" name = "actix-testing"
version = "1.0.1" version = "1.0.1"
@ -341,7 +352,7 @@ dependencies = [
"actix-macros", "actix-macros",
"actix-rt", "actix-rt",
"actix-server", "actix-server",
"actix-service", "actix-service 1.0.6",
"log", "log",
"socket2", "socket2",
] ]
@ -368,7 +379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24789b7d7361cf5503a504ebe1c10806896f61e96eca9a7350e23001aca715fb" checksum = "24789b7d7361cf5503a504ebe1c10806896f61e96eca9a7350e23001aca715fb"
dependencies = [ dependencies = [
"actix-codec 0.3.0", "actix-codec 0.3.0",
"actix-service", "actix-service 1.0.6",
"actix-utils 2.0.0", "actix-utils 2.0.0",
"futures-util", "futures-util",
] ]
@ -381,11 +392,11 @@ checksum = "fcf8f5631bf01adec2267808f00e228b761c60c0584cc9fa0b5364f41d147f4e"
dependencies = [ dependencies = [
"actix-codec 0.2.0", "actix-codec 0.2.0",
"actix-rt", "actix-rt",
"actix-service", "actix-service 1.0.6",
"bitflags", "bitflags",
"bytes 0.5.6", "bytes 0.5.6",
"either", "either",
"futures 0.3.14", "futures",
"log", "log",
"pin-project 0.4.28", "pin-project 0.4.28",
"slab", "slab",
@ -399,7 +410,7 @@ checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a"
dependencies = [ dependencies = [
"actix-codec 0.3.0", "actix-codec 0.3.0",
"actix-rt", "actix-rt",
"actix-service", "actix-service 1.0.6",
"bitflags", "bitflags",
"bytes 0.5.6", "bytes 0.5.6",
"either", "either",
@ -423,7 +434,7 @@ dependencies = [
"actix-router", "actix-router",
"actix-rt", "actix-rt",
"actix-server", "actix-server",
"actix-service", "actix-service 1.0.6",
"actix-testing", "actix-testing",
"actix-threadpool", "actix-threadpool",
"actix-tls", "actix-tls",
@ -439,7 +450,7 @@ dependencies = [
"fxhash", "fxhash",
"log", "log",
"mime", "mime",
"pin-project 1.0.6", "pin-project 1.0.7",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
@ -518,11 +529,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"actix 0.10.0", "actix 0.10.0",
"actix-rt", "actix-rt",
"actix-service", "actix-service 2.0.0",
"actix-web-actors", "actix-web-actors",
"bytes 0.5.6", "bytes 0.5.6",
"env_logger", "env_logger",
"futures 0.3.14", "futures",
"jirs-config", "jirs-config",
"libc", "libc",
"log", "log",
@ -545,18 +556,6 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]] [[package]]
name = "ascii_utils" name = "ascii_utils"
version = "0.9.3" version = "0.9.3"
@ -565,9 +564,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.49" version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589652ce7ccb335d1e7ecb3be145425702b290dbcb7029bbeaae263fc1d87b48" checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -606,7 +605,7 @@ dependencies = [
"actix-codec 0.3.0", "actix-codec 0.3.0",
"actix-http 2.2.0", "actix-http 2.2.0",
"actix-rt", "actix-rt",
"actix-service", "actix-service 1.0.6",
"base64 0.13.0", "base64 0.13.0",
"bytes 0.5.6", "bytes 0.5.6",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@ -623,11 +622,12 @@ dependencies = [
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.56" version = "0.3.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" checksum = "88fb5a785d6b44fd9d6700935608639af1b8356de1e55d5f7c2740f4faa15d82"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"miniz_oxide", "miniz_oxide",
@ -704,17 +704,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "blake2b_simd"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.7.3" version = "0.7.3"
@ -914,15 +903,9 @@ dependencies = [
[[package]] [[package]]
name = "const_fn" name = "const_fn"
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 = "076a6803b0dacd6a88cfe64deba628b01533ff5ef265687e6938280c1afd0a28" checksum = "402da840495de3f976eaefc3485b7f5eb5b0bf9761f9a47be27fe975b3b8c2ec"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
@ -984,7 +967,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
dependencies = [ dependencies = [
"crossbeam-utils 0.7.2", "crossbeam-utils",
"maybe-uninit", "maybe-uninit",
] ]
@ -999,17 +982,6 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
dependencies = [
"autocfg 1.0.1",
"cfg-if 1.0.0",
"lazy_static",
]
[[package]] [[package]]
name = "crypto-mac" name = "crypto-mac"
version = "0.8.0" version = "0.8.0"
@ -1035,7 +1007,7 @@ dependencies = [
"diesel", "diesel",
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures 0.3.14", "futures",
"ipnetwork 0.16.0", "ipnetwork 0.16.0",
"jirs-config", "jirs-config",
"jirs-data", "jirs-data",
@ -1158,9 +1130,9 @@ dependencies = [
[[package]] [[package]]
name = "dirs-sys" name = "dirs-sys"
version = "0.3.5" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
dependencies = [ dependencies = [
"libc", "libc",
"redox_users", "redox_users",
@ -1361,7 +1333,7 @@ dependencies = [
"actix-files", "actix-files",
"bytes 0.5.6", "bytes 0.5.6",
"env_logger", "env_logger",
"futures 0.3.14", "futures",
"jirs-config", "jirs-config",
"log", "log",
"pretty_env_logger", "pretty_env_logger",
@ -1433,12 +1405,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "futures"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.14" version = "0.3.14"
@ -1757,9 +1723,9 @@ dependencies = [
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.3.6" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc35c995b9d93ec174cf9a27d425c7892722101e14993cd227fdb51d70cf9589" checksum = "4a1ce40d6fc9764887c2fdc7305c3dcc429ba11ff981c1509416afd5697e4437"
[[package]] [[package]]
name = "httpdate" name = "httpdate"
@ -1792,7 +1758,7 @@ dependencies = [
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project 1.0.6", "pin-project 1.0.7",
"socket2", "socket2",
"tokio", "tokio",
"tower-service", "tower-service",
@ -1941,7 +1907,7 @@ dependencies = [
"actix 0.10.0", "actix 0.10.0",
"actix-cors", "actix-cors",
"actix-rt", "actix-rt",
"actix-service", "actix-service 2.0.0",
"actix-web", "actix-web",
"actix-web-actors", "actix-web-actors",
"amazon-actor", "amazon-actor",
@ -1955,7 +1921,7 @@ dependencies = [
"dotenv", "dotenv",
"env_logger", "env_logger",
"filesystem-actor", "filesystem-actor",
"futures 0.3.14", "futures",
"highlight-actor", "highlight-actor",
"ipnetwork 0.16.0", "ipnetwork 0.16.0",
"jirs-config", "jirs-config",
@ -1989,15 +1955,18 @@ dependencies = [
"derive_enum_iter", "derive_enum_iter",
"derive_enum_primitive", "derive_enum_primitive",
"dotenv", "dotenv",
"futures 0.1.31", "futures",
"jirs-data", "jirs-data",
"js-sys", "js-sys",
"log",
"seed", "seed",
"serde", "serde",
"serde_json", "serde_json",
"uuid 0.8.2", "uuid 0.8.2",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test", "wasm-bindgen-test",
"wasm-logger",
"web-sys", "web-sys",
"wee_alloc", "wee_alloc",
] ]
@ -2073,9 +2042,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.93" version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
[[package]] [[package]]
name = "line-wrap" name = "line-wrap"
@ -2135,7 +2104,7 @@ dependencies = [
"actix 0.10.0", "actix 0.10.0",
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures 0.3.14", "futures",
"jirs-config", "jirs-config",
"lettre", "lettre",
"lettre_email", "lettre_email",
@ -2502,6 +2471,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "paste"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.1.0" version = "2.1.0"
@ -2562,11 +2537,11 @@ dependencies = [
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.0.6" version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6" checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4"
dependencies = [ dependencies = [
"pin-project-internal 1.0.6", "pin-project-internal 1.0.7",
] ]
[[package]] [[package]]
@ -2582,9 +2557,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.0.6" version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5" checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2948,20 +2923,19 @@ dependencies = [
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.3.5" 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 = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [ dependencies = [
"getrandom 0.1.16", "getrandom 0.2.2",
"redox_syscall 0.1.57", "redox_syscall 0.2.6",
"rust-argon2",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.5" version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -3013,7 +2987,7 @@ dependencies = [
"base64 0.12.3", "base64 0.12.3",
"bytes 0.5.6", "bytes 0.5.6",
"crc32fast", "crc32fast",
"futures 0.3.14", "futures",
"http", "http",
"hyper", "hyper",
"hyper-tls", "hyper-tls",
@ -3040,7 +3014,7 @@ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
"dirs", "dirs",
"futures 0.3.14", "futures",
"hyper", "hyper",
"pin-project 0.4.28", "pin-project 0.4.28",
"regex", "regex",
@ -3059,7 +3033,7 @@ checksum = "1146e37a7c1df56471ea67825fe09bbbd37984b5f6e201d8b2e0be4ee15643d8"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes 0.5.6", "bytes 0.5.6",
"futures 0.3.14", "futures",
"rusoto_core", "rusoto_core",
"xml-rs", "xml-rs",
] ]
@ -3072,7 +3046,7 @@ checksum = "97a740a88dde8ded81b6f2cff9cd5e054a5a2e38a38397260f7acdd2c85d17dd"
dependencies = [ dependencies = [
"base64 0.12.3", "base64 0.12.3",
"bytes 0.5.6", "bytes 0.5.6",
"futures 0.3.14", "futures",
"hex", "hex",
"hmac", "hmac",
"http", "http",
@ -3089,18 +3063,6 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "rust-argon2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
dependencies = [
"base64 0.13.0",
"blake2b_simd",
"constant_time_eq",
"crossbeam-utils 0.8.3",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.18" version = "0.1.18"
@ -3194,13 +3156,13 @@ dependencies = [
[[package]] [[package]]
name = "seed" name = "seed"
version = "0.8.0" version = "0.8.0"
source = "git+https://github.com/seed-rs/seed.git#e1d82f5012fb9e71b36be3a5457d61d34a1ee53d" source = "git+https://github.com/seed-rs/seed.git#938d71b6527efcd84c7d9ff37330949791b6d3a4"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"cookie", "cookie",
"dbg", "dbg",
"enclose", "enclose",
"futures 0.3.14", "futures",
"getrandom 0.2.2", "getrandom 0.2.2",
"gloo-file", "gloo-file",
"gloo-timers", "gloo-timers",
@ -3363,9 +3325,9 @@ dependencies = [
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.2" 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 = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
@ -3468,9 +3430,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.69" version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3763,7 +3725,7 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [ dependencies = [
"pin-project 1.0.6", "pin-project 1.0.7",
"tracing", "tracing",
] ]
@ -3782,7 +3744,7 @@ dependencies = [
"async-trait", "async-trait",
"enum-as-inner", "enum-as-inner",
"failure", "failure",
"futures 0.3.14", "futures",
"idna", "idna",
"lazy_static", "lazy_static",
"log", "log",
@ -3802,7 +3764,7 @@ dependencies = [
"async-trait", "async-trait",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"enum-as-inner", "enum-as-inner",
"futures 0.3.14", "futures",
"idna", "idna",
"lazy_static", "lazy_static",
"log", "log",
@ -3821,7 +3783,7 @@ checksum = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if 0.1.10",
"failure", "failure",
"futures 0.3.14", "futures",
"ipconfig", "ipconfig",
"lazy_static", "lazy_static",
"log", "log",
@ -3839,7 +3801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "710f593b371175db53a26d0b38ed2978fafb9e9e8d3868b1acd753ea18df0ceb" checksum = "710f593b371175db53a26d0b38ed2978fafb9e9e8d3868b1acd753ea18df0ceb"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if 0.1.10",
"futures 0.3.14", "futures",
"ipconfig", "ipconfig",
"lazy_static", "lazy_static",
"log", "log",
@ -4032,9 +3994,9 @@ dependencies = [
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.11" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d"
[[package]] [[package]]
name = "vec_map" name = "vec_map"
@ -4179,6 +4141,17 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "wasm-logger"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718"
dependencies = [
"log",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "web-actor" name = "web-actor"
version = "0.1.0" version = "0.1.0"
@ -4187,7 +4160,7 @@ dependencies = [
"actix-cors", "actix-cors",
"actix-multipart", "actix-multipart",
"actix-rt", "actix-rt",
"actix-service", "actix-service 2.0.0",
"actix-web", "actix-web",
"actix-web-actors", "actix-web-actors",
"amazon-actor", "amazon-actor",
@ -4196,7 +4169,7 @@ dependencies = [
"database-actor", "database-actor",
"env_logger", "env_logger",
"filesystem-actor", "filesystem-actor",
"futures 0.3.14", "futures",
"jirs-config", "jirs-config",
"jirs-data", "jirs-data",
"libc", "libc",
@ -4233,7 +4206,7 @@ dependencies = [
"database-actor", "database-actor",
"env_logger", "env_logger",
"flate2", "flate2",
"futures 0.3.14", "futures",
"highlight-actor", "highlight-actor",
"jirs-config", "jirs-config",
"jirs-data", "jirs-data",
@ -4363,6 +4336,6 @@ dependencies = [
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.2.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"

View File

@ -58,12 +58,12 @@ https://git.sr.ht/~tsumanu/jirs
* [X] Add Epic * [X] Add Epic
* [X] Edit Epic * [X] Edit Epic
* [X] Delete Epic * [X] Delete Epic
* [ ] Epic `starts` and `ends` date * [X] Epic `starts` and `ends` date
* [X] Grouping by Epic * [X] Grouping by Epic
* [ ] Basic Rich Text Editor * [X] Basic Rich Text Editor
* [ ] Insert Code in Rich Text Editor * [ ] Insert Code in Rich Text Editor
* [X] Code syntax * [X] Code syntax
* [ ] Personal settings to choose MDE (Markdown Editor) or RTE * [X] Personal settings to choose MDE (Markdown Editor) or RTE
* [ ] Issues and filters view * [ ] Issues and filters view
* [ ] Issues and filters working filters * [ ] Issues and filters working filters

View File

@ -1,6 +1,6 @@
use derive_db_execute::Execute; use derive_db_execute::Execute;
use diesel::prelude::*; use diesel::prelude::*;
use jirs_data::{DescriptionString, Epic, EpicId, ProjectId}; use jirs_data::{DescriptionString, EndsAt, Epic, EpicId, ProjectId, StartsAt};
use crate::{db_create, db_delete, db_load, db_update}; use crate::{db_create, db_delete, db_load, db_update};
@ -35,7 +35,7 @@ db_create! {
} }
db_update! { db_update! {
UpdateEpic, UpdateEpicName,
msg => epics => diesel::update( msg => epics => diesel::update(
epics epics
.filter(project_id.eq(msg.project_id)) .filter(project_id.eq(msg.project_id))
@ -47,6 +47,32 @@ db_update! {
name => String name => String
} }
db_update! {
UpdateEpicStartsAt,
msg => epics => diesel::update(
epics
.filter(project_id.eq(msg.project_id))
.find(msg.epic_id),
).set(starts_at.eq(msg.starts_at)),
Epic,
epic_id => i32,
project_id => i32,
starts_at => Option<StartsAt>
}
db_update! {
UpdateEpicEndsAt,
msg => epics => diesel::update(
epics
.filter(project_id.eq(msg.project_id))
.find(msg.epic_id),
).set(ends_at.eq(msg.ends_at)),
Epic,
epic_id => i32,
project_id => i32,
ends_at => Option<EndsAt>
}
db_delete! { db_delete! {
DeleteEpic, DeleteEpic,
msg => epics => diesel::delete( msg => epics => diesel::delete(

View File

@ -20,6 +20,7 @@ pub enum ResourceKind {
Project, Project,
Token, Token,
UserProject, UserProject,
UserSetting,
User, User,
Comment, Comment,
} }

View File

@ -23,6 +23,7 @@ pub mod projects;
pub mod schema; pub mod schema;
pub mod tokens; pub mod tokens;
pub mod user_projects; pub mod user_projects;
pub mod user_settings;
pub mod users; pub mod users;
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>; pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;

View File

@ -620,6 +620,35 @@ table! {
} }
} }
table! {
use diesel::sql_types::*;
use jirs_data::*;
/// Representation of the `user_settings` table.
///
/// (Automatically generated by Diesel.)
user_settings (id) {
/// The `id` column of the `user_settings` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
id -> Int4,
/// The `user_id` column of the `user_settings` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
user_id -> Int4,
/// The `text_editor_mode` column of the `user_settings` table.
///
/// Its SQL type is `TextEditorModeType`.
///
/// (Automatically generated by Diesel.)
text_editor_mode -> TextEditorModeType,
}
}
table! { table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use jirs_data::*; use jirs_data::*;
@ -683,6 +712,7 @@ joinable!(issues -> users (reporter_id));
joinable!(tokens -> users (user_id)); joinable!(tokens -> users (user_id));
joinable!(user_projects -> projects (project_id)); joinable!(user_projects -> projects (project_id));
joinable!(user_projects -> users (user_id)); joinable!(user_projects -> users (user_id));
joinable!(user_settings -> users (user_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
comments, comments,
@ -695,5 +725,6 @@ allow_tables_to_appear_in_same_query!(
projects, projects,
tokens, tokens,
user_projects, user_projects,
user_settings,
users, users,
); );

View File

@ -0,0 +1,57 @@
use diesel::prelude::*;
use jirs_data::{TextEditorMode, UserId, UserSetting};
use crate::{db_find, db_update};
db_find! {
FindUserSetting,
msg => user_settings => user_settings
.distinct_on(id)
.filter(user_id.eq(msg.user_id))
.limit(1),
UserSetting,
user_id => UserId
}
db_update! {
UpdateUserSetting,
msg => conn => user_settings => {
inner::Update { user_id: msg.user_id, mode: msg.mode }
.execute(conn).or_else(|_|
inner::Create { user_id: msg.user_id, mode: msg.mode }
.execute(conn)
)?;
user_settings.filter(user_id.eq(msg.user_id))
},
UserSetting,
user_id => UserId,
mode => TextEditorMode
}
mod inner {
use diesel::prelude::*;
use jirs_data::{TextEditorMode, UserId, UserSetting};
use crate::{db_create, db_update};
db_update! {
Update,
msg => user_settings => {
diesel::update(user_settings.filter(user_id.eq(msg.user_id))).set(text_editor_mode.eq(msg.mode))
},
UserSetting,
user_id => UserId,
mode => TextEditorMode
}
db_create! {
Create,
msg => user_settings => diesel::insert_into(user_settings).values((
user_id.eq(msg.user_id),
text_editor_mode.eq(msg.mode)
)),
UserSetting,
user_id => UserId,
mode => TextEditorMode
}
}

View File

@ -1,84 +1,5 @@
use std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::header::{self}; use actix_web::http::header::{self};
use actix_web::http::HeaderMap; use actix_web::http::HeaderMap;
use actix_web::Error;
use futures::future::{ok, FutureExt, LocalBoxFuture, Ready};
use jirs_data::User;
type Db = actix_web::web::Data<database_actor::DbPool>;
#[derive(Default)]
pub struct Authorize;
impl<S, B> Transform<S> for Authorize
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = AuthorizeMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(AuthorizeMiddleware { service })
}
}
pub struct AuthorizeMiddleware<S> {
service: S,
}
impl<S, B> Service for AuthorizeMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
let pool: &Db = match req.app_data::<Db>() {
Some(d) => d,
_ => {
return async move {
let res = crate::errors::ServiceError::DatabaseConnectionLost
.into_http_response()
.into_body();
Ok(req.into_response(res))
}
.boxed_local();
}
};
match check_token(req.headers(), pool.clone()) {
std::result::Result::Err(e) => {
return async move {
let res = e.into_http_response().into_body();
Ok(req.into_response(res))
}
.boxed_local();
}
Ok(_user) => {}
};
let fut = self.service.call(req);
async move { fut.await }.boxed_local()
}
}
pub fn token_from_headers( pub fn token_from_headers(
headers: &HeaderMap, headers: &HeaderMap,
@ -90,21 +11,6 @@ pub fn token_from_headers(
.and_then(|s| parse_bearer(s)) .and_then(|s| parse_bearer(s))
} }
fn check_token(
headers: &HeaderMap,
pool: Db,
) -> std::result::Result<User, crate::errors::ServiceError> {
token_from_headers(headers).and_then(|access_token| {
use database_actor::authorize_user::AuthorizeUser;
let conn = pool
.get()
.map_err(|_| crate::errors::ServiceError::DatabaseConnectionLost)?;
AuthorizeUser { access_token }
.execute(&conn)
.map_err(|_| crate::errors::ServiceError::Unauthorized)
})
}
fn parse_bearer(header: &str) -> Result<uuid::Uuid, crate::errors::ServiceError> { fn parse_bearer(header: &str) -> Result<uuid::Uuid, crate::errors::ServiceError> {
if !header.starts_with("Bearer ") { if !header.starts_with("Bearer ") {
return Err(crate::errors::ServiceError::Unauthorized); return Err(crate::errors::ServiceError::Unauthorized);

View File

@ -3,11 +3,13 @@ use database_actor::authorize_user::AuthorizeUser;
use database_actor::tokens::{CreateBindToken, FindBindToken}; use database_actor::tokens::{CreateBindToken, FindBindToken};
use database_actor::users::LookupUser; use database_actor::users::LookupUser;
use futures::executor::block_on; use futures::executor::block_on;
use jirs_data::msg::WsError;
use jirs_data::{Token, WsMsg}; use jirs_data::{Token, WsMsg};
use mail_actor::welcome::Welcome; use mail_actor::welcome::Welcome;
use crate::{ use crate::{
db_or_debug_and_return, mail_or_debug_and_return, WebSocketActor, WsHandler, WsResult, db_or_debug_and_return, db_or_debug_or_fallback, mail_or_debug_and_return, WebSocketActor,
WsHandler, WsResult,
}; };
pub struct Authenticate { pub struct Authenticate {
@ -19,7 +21,12 @@ impl WsHandler<Authenticate> for WebSocketActor {
fn handle_msg(&mut self, msg: Authenticate, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: Authenticate, _ctx: &mut Self::Context) -> WsResult {
let Authenticate { name, email } = msg; let Authenticate { name, email } = msg;
// TODO check attempt number, allow only 5 times per day // TODO check attempt number, allow only 5 times per day
let user = db_or_debug_and_return!(self, LookupUser { name, email }); let user = db_or_debug_and_return!(
self,
LookupUser { name, email },
Ok(Some(WsMsg::Error(WsError::InvalidLoginPair))),
Ok(Some(WsMsg::Error(WsError::InvalidLoginPair)))
);
let token = db_or_debug_and_return!(self, CreateBindToken { user_id: user.id }); let token = db_or_debug_and_return!(self, CreateBindToken { user_id: user.id });
if let Some(bind_token) = token.bind_token.as_ref().cloned() { if let Some(bind_token) = token.bind_token.as_ref().cloned() {
let _ = mail_or_debug_and_return!( let _ = mail_or_debug_and_return!(
@ -50,12 +57,20 @@ impl WsHandler<CheckAuthToken> for WebSocketActor {
)))), )))),
Ok(Some(WsMsg::AuthorizeExpired)) Ok(Some(WsMsg::AuthorizeExpired))
); );
let setting: jirs_data::UserSetting = db_or_debug_or_fallback!(
self,
database_actor::user_settings::FindUserSetting { user_id: user.id },
crate::user_settings::default_user_setting(user.id),
crate::user_settings::default_user_setting(user.id)
);
self.current_user = Some(user.clone()); self.current_user = Some(user.clone());
self.current_user_project = self.load_user_project().ok(); self.current_user_project = self.load_user_project().ok();
self.current_project = self.load_project().ok(); self.current_project = self.load_project().ok();
block_on(self.join_channel(ctx.address().recipient())); block_on(self.join_channel(ctx.address().recipient()));
Ok(Some(WsMsg::AuthorizeLoaded(Ok(user)))) Ok(Some(WsMsg::AuthorizeLoaded(Ok((user, setting)))))
} }
} }

View File

@ -1,5 +1,7 @@
use futures::executor::block_on; use futures::executor::block_on;
use jirs_data::{DescriptionString, EpicId, IssueType, NameString, UserProject, WsMsg}; use jirs_data::{
DescriptionString, EndsAt, EpicId, IssueType, NameString, StartsAt, UserProject, WsMsg,
};
use crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}; use crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult};
@ -45,21 +47,60 @@ impl WsHandler<CreateEpic> for WebSocketActor {
} }
} }
pub struct UpdateEpic { pub struct UpdateEpicName {
pub epic_id: EpicId, pub epic_id: EpicId,
pub name: NameString, pub name: NameString,
} }
impl WsHandler<UpdateEpic> for WebSocketActor { impl WsHandler<UpdateEpicName> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: UpdateEpicName, _ctx: &mut Self::Context) -> WsResult {
let UpdateEpic { epic_id, name } = msg;
let UserProject { project_id, .. } = self.require_user_project()?; let UserProject { project_id, .. } = self.require_user_project()?;
let epic = db_or_debug_and_return!( let epic = db_or_debug_and_return!(
self, self,
database_actor::epics::UpdateEpic { database_actor::epics::UpdateEpicName {
project_id: *project_id, project_id: *project_id,
epic_id, epic_id: msg.epic_id,
name: name.clone(), name: msg.name.clone(),
}
);
Ok(Some(WsMsg::EpicUpdated(epic)))
}
}
pub struct UpdateEpicStartsAt {
pub epic_id: EpicId,
pub starts_at: Option<StartsAt>,
}
impl WsHandler<UpdateEpicStartsAt> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateEpicStartsAt, _ctx: &mut Self::Context) -> WsResult {
let UserProject { project_id, .. } = self.require_user_project()?;
let epic = db_or_debug_and_return!(
self,
database_actor::epics::UpdateEpicStartsAt {
project_id: *project_id,
epic_id: msg.epic_id,
starts_at: msg.starts_at,
}
);
Ok(Some(WsMsg::EpicUpdated(epic)))
}
}
pub struct UpdateEpicEndsAt {
pub epic_id: EpicId,
pub ends_at: Option<EndsAt>,
}
impl WsHandler<UpdateEpicEndsAt> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateEpicEndsAt, _ctx: &mut Self::Context) -> WsResult {
let UserProject { project_id, .. } = self.require_user_project()?;
let epic = db_or_debug_and_return!(
self,
database_actor::epics::UpdateEpicEndsAt {
project_id: *project_id,
epic_id: msg.epic_id,
ends_at: msg.ends_at,
} }
); );
Ok(Some(WsMsg::EpicUpdated(epic))) Ok(Some(WsMsg::EpicUpdated(epic)))

View File

@ -20,4 +20,5 @@ pub mod issues;
pub mod messages; pub mod messages;
pub mod projects; pub mod projects;
pub mod user_projects; pub mod user_projects;
pub mod user_settings;
pub mod users; pub mod users;

View File

@ -0,0 +1,30 @@
use futures::executor::block_on;
use jirs_data::{TextEditorMode, UserId, UserSetting, WsMsg};
use crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult};
pub fn default_user_setting(user_id: UserId) -> UserSetting {
UserSetting {
id: 0,
user_id,
text_editor_mode: Default::default(),
}
}
pub struct SetTextEditorMode {
pub mode: TextEditorMode,
}
impl WsHandler<SetTextEditorMode> for WebSocketActor {
fn handle_msg(&mut self, msg: SetTextEditorMode, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id;
let setting = db_or_debug_and_return!(
self,
database_actor::user_settings::UpdateUserSetting {
user_id,
mode: msg.mode
}
);
Ok(Some(WsMsg::UserSettingUpdated(setting)))
}
}

View File

@ -149,6 +149,11 @@ impl WebSocketActor {
self.handle_msg(RemoveInvitedUser { user_id }, ctx)? self.handle_msg(RemoveInvitedUser { user_id }, ctx)?
} }
// user settings
WsMsg::UserSettingSetEditorMode(mode) => {
self.handle_msg(user_settings::SetTextEditorMode { mode }, ctx)?
}
// comments // comments
WsMsg::IssueCommentsLoad(issue_id) => { WsMsg::IssueCommentsLoad(issue_id) => {
self.handle_msg(LoadIssueComments { issue_id }, ctx)? self.handle_msg(LoadIssueComments { issue_id }, ctx)?
@ -189,8 +194,14 @@ impl WebSocketActor {
}, },
ctx, ctx,
)?, )?,
WsMsg::EpicUpdate(epic_id, name) => { WsMsg::EpicUpdateName(epic_id, name) => {
self.handle_msg(epics::UpdateEpic { epic_id, name }, ctx)? self.handle_msg(epics::UpdateEpicName { epic_id, name }, ctx)?
}
WsMsg::EpicUpdateStartsAt(epic_id, starts_at) => {
self.handle_msg(epics::UpdateEpicStartsAt { epic_id, starts_at }, ctx)?
}
WsMsg::EpicUpdateEndsAt(epic_id, ends_at) => {
self.handle_msg(epics::UpdateEpicEndsAt { epic_id, ends_at }, 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::EpicTransform(epic_id, issue_type) => self.handle_msg( WsMsg::EpicTransform(epic_id, issue_type) => self.handle_msg(

View File

@ -8,6 +8,16 @@ macro_rules! db_or_debug_and_return {
}; };
} }
#[macro_export]
macro_rules! db_or_debug_or_fallback {
($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
$crate::actor_or_debug_and_fallback!($s, db, $msg, $actor_err, $mailbox_err)
};
($s: ident, $msg: expr) => {
$crate::actor_or_debug_and_fallback!($s, db, $msg)
};
}
#[macro_export] #[macro_export]
macro_rules! mail_or_debug_and_return { macro_rules! mail_or_debug_and_return {
($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => { ($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {

View File

@ -28,9 +28,11 @@ 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.3.6"
dotenv = { version = "*" } dotenv = { version = "*" }
wasm-logger = { version = "*" }
log = "*"
[dependencies.wee_alloc] [dependencies.wee_alloc]
version = "*" version = "*"
@ -40,6 +42,9 @@ features = ["static_array_backend"]
version = "*" version = "*"
features = ["enable-interning"] features = ["enable-interning"]
[dependencies.wasm-bindgen-futures]
version = "*"
[dependencies.js-sys] [dependencies.js-sys]
version = "*" version = "*"
default-features = false default-features = false

View File

@ -0,0 +1,11 @@
#profile {
> .formContainer {
display: flex;
justify-content: center;
.styledForm {
max-width: 1024px;
width: 100%;
}
}
}

View File

@ -106,6 +106,26 @@
> .epicName { > .epicName {
} }
> .timeRange {
display: flex;
justify-content: space-between;
> * {
margin-left: 2px;
margin-right: 2px;
}
> .endsAt {
&.error {
color: var(--danger);
}
&.warning {
color: var(--warning);
}
}
}
> .epicActions { > .epicActions {
> .styledButton { > .styledButton {
> .styledIcon { > .styledIcon {

View File

@ -1,85 +1,113 @@
.styledEditor { .styledEditor {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
max-width: 100%; max-width: 100%;
> input[type="radio"] { > input[type="radio"] {
display: none; display: none;
}
> .navbar {
border: 1px solid var(--borderLight);
border-top-left-radius: 5px;
border-top-right-radius: 5px;
font-family: var(--font-medium);
font-weight: normal;
text-align: center;
height: 32px;
vertical-align: middle;
line-height: 2;
white-space: nowrap;
transition: all 0.1s;
appearance: none;
cursor: pointer;
user-select: none;
font-size: 14.5px;
border-color: var(--borderInputFocus);
&:not(:hover) {
border-color: var(--backgroundLightest);
background-color: var(--borderLight);
} }
> .navbar { &:hover {
border: 1px solid var(--borderLight); background: #fff;
border-top-left-radius: 5px; border: 1px solid var(--borderInputFocus);
border-top-right-radius: 5px; box-shadow: 0 0 0 1px var(--borderInputFocus);
font-family: var(--font-medium); }
font-weight: normal; }
text-align: center;
height: 32px;
vertical-align: middle;
line-height: 2;
white-space: nowrap;
transition: all 0.1s;
appearance: none;
cursor: pointer;
user-select: none;
font-size: 14.5px;
border-color: var(--borderInputFocus);
&:not(:hover) { > .navbar.activeTab {
border-color: var(--backgroundLightest); background-color: var(--backgroundLightest);
background-color: var(--borderLight); border-color: var(--borderLight);
}
> .navbar.editorTab {
min-width: 50%;
}
> .navbar.viewTab {
min-width: 50%;
}
> .styledTextArea {
grid-area: view;
display: none;
}
> .view {
min-width: 100%;
display: none;
min-height: 40px;
padding-top: 15px;
}
> input.editorRadio {
&:checked {
~ {
.styledTextArea {
display: block;
} }
}
}
}
&:hover { > input.viewRadio {
background: #fff; &:checked {
border: 1px solid var(--borderInputFocus); ~ {
box-shadow: 0 0 0 1px var(--borderInputFocus); .view {
} display: block;
}
> .navbar.activeTab {
background-color: var(--backgroundLightest);
border-color: var(--borderLight);
}
> .navbar.editorTab {
min-width: 50%;
}
> .navbar.viewTab {
min-width: 50%;
}
> .styledTextArea {
grid-area: view;
display: none;
}
> .view {
min-width: 100%;
display: none;
min-height: 40px;
padding-top: 15px;
}
> input.editorRadio {
&:checked {
~ {
.styledTextArea {
display: block;
}
}
}
}
> input.viewRadio {
&:checked {
~ {
.view {
display: block;
}
}
} }
}
} }
}
}
.styledCheckbox.textEditorModeSwitcher {
justify-content: end;
margin-top: 5px;
margin-bottom: 5px;
& > .styledCheckboxChild {
&.mdonly, &.rteonly {
display: flex;
margin-left: 5px;
border: none;
color: var(--textDark);
&.selected {
color: var(--borderInputFocus);
.styledIcon {
color: var(--borderInputFocus);
}
}
.styledIcon {
font-size: 24px;
color: var(--textDark);
}
}
}
} }

View File

@ -48,11 +48,17 @@
} }
.styledInput.invalid { .styledInput.invalid {
input {
border: 1px solid var(--danger); border: 1px solid var(--danger);
box-shadow: none; box-shadow: none;
&:focus { &:focus {
border: 1px solid var(--danger); border: 1px solid var(--danger);
box-shadow: none; box-shadow: none;
} }
}
.error {
color: var(--danger);
}
} }

View File

@ -34,3 +34,4 @@
@import "css/users.scss"; @import "css/users.scss";
@import "css/invite.scss"; @import "css/invite.scss";
@import "css/reports.scss"; @import "css/reports.scss";
@import "css/profile.scss";

View File

@ -1,7 +1,7 @@
use jirs_data::{EpicId, IssueStatusId, WsMsg}; use jirs_data::{EpicId, IssueStatusId, WsMsg};
use seed::prelude::WebSocketMessage; use seed::prelude::WebSocketMessage;
use crate::components::styled_editor::Mode as TabMode; use crate::components::styled_md_editor::MdEditorMode as TabMode;
use crate::FieldId; use crate::FieldId;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View File

@ -10,9 +10,11 @@ pub mod styled_icon;
pub mod styled_image_input; pub mod styled_image_input;
pub mod styled_input; pub mod styled_input;
pub mod styled_link; pub mod styled_link;
pub mod styled_md_editor;
pub mod styled_modal; pub mod styled_modal;
// pub mod styled_rte; pub mod styled_rte;
pub mod styled_select; pub mod styled_select;
pub mod styled_select_child; pub mod styled_select_child;
pub mod styled_textarea; pub mod styled_textarea;
pub mod styled_tip;
pub mod styled_tooltip; pub mod styled_tooltip;

View File

@ -34,6 +34,7 @@ pub struct ChildBuilder<'l> {
pub value: u32, pub value: u32,
pub selected: bool, pub selected: bool,
pub class_list: &'l str, pub class_list: &'l str,
pub icon: Node<Msg>,
} }
impl<'l> Default for ChildBuilder<'l> { impl<'l> Default for ChildBuilder<'l> {
@ -46,6 +47,7 @@ impl<'l> Default for ChildBuilder<'l> {
value: 0, value: 0,
selected: false, selected: false,
class_list: "", class_list: "",
icon: Node::Empty,
} }
} }
} }
@ -53,32 +55,32 @@ impl<'l> Default for ChildBuilder<'l> {
impl<'l> ChildBuilder<'l> { impl<'l> ChildBuilder<'l> {
#[inline(always)] #[inline(always)]
pub fn render(self) -> Node<Msg> { pub fn render(self) -> Node<Msg> {
let ChildBuilder { let id = self.field_id.to_string();
field_id,
name,
label,
value,
selected,
class_list,
} = self;
let id = field_id.to_string();
let handler: EventHandler<Msg> = { let handler: EventHandler<Msg> = {
let id = field_id; let id = self.field_id;
mouse_ev(Ev::Click, move |_| Msg::U32InputChanged(id, value)) let value = self.value;
mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
ev.prevent_default();
Msg::U32InputChanged(id, value)
})
}; };
div![ div![
C![ C![
"styledCheckboxChild", "styledCheckboxChild",
class_list, self.class_list,
IF![selected => "selected"] IF![self.selected => "selected"]
], ],
handler, handler,
label![attrs![At::For => format!("{}-{}", id, name)], label], self.icon,
label![
attrs![At::For => format!("{}-{}", id, self.name)],
self.label
],
input![ input![
attrs![At::Type => "radio", At::Name => name, At::Id => format!("{}-{}", id, name)], attrs![At::Type => "radio", At::Name => self.name, At::Id => format!("{}-{}", id, self.name)],
IF![selected => attrs!(At::Checked => selected)] IF![self.selected => attrs!(At::Checked => self.selected)]
], ],
] ]
} }

View File

@ -19,7 +19,7 @@ pub enum StyledDateTimeChanged {
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Clone, Debug, PartialOrd, PartialEq)]
pub struct StyledDateTimeInputState { pub struct StyledDateTimeInputState {
field_id: FieldId, pub field_id: FieldId,
pub timestamp: Option<chrono::NaiveDateTime>, pub timestamp: Option<chrono::NaiveDateTime>,
pub popup_visible: bool, pub popup_visible: bool,
} }
@ -187,7 +187,7 @@ impl StyledDateTimeInput {
children: vec![ children: vec![
h2![ h2![
left_action, left_action,
span![current.format("%B %Y").to_string()], span![start.format("%B %Y").to_string()],
right_action right_action
], ],
div![ div![
@ -285,6 +285,7 @@ impl<'l> DayCell<'l> {
ev(Ev::Click, move |ev| { ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
log::info!("{:?}", date);
Msg::StyledDateTimeInputChanged( Msg::StyledDateTimeInputChanged(
field_id, field_id,
StyledDateTimeChanged::DayChanged(Some(date)), StyledDateTimeChanged::DayChanged(Some(date)),

View File

@ -1,124 +1,181 @@
use jirs_data::TextEditorMode;
use seed::prelude::*; use seed::prelude::*;
use seed::*; use seed::*;
use crate::components::styled_textarea::StyledTextarea; use crate::components::styled_checkbox::{ChildBuilder, StyledCheckbox, StyledCheckboxState};
use crate::{FieldChange, FieldId, Msg}; use crate::components::styled_icon::{Icon, StyledIcon};
use crate::components::styled_md_editor::{MdEditorMode, StyledMdEditor, StyledMdEditorState};
use crate::components::styled_rte::{StyledRte, StyledRteState};
use crate::{FieldId, Msg};
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)] #[derive(Debug)]
#[repr(C)] pub enum EditorMode {
pub enum Mode { Md(StyledMdEditorState),
Editor, Rte(StyledRteState),
View,
} }
#[derive(Debug, Clone, PartialOrd, PartialEq)] #[derive(Debug)]
pub struct StyledEditorState { pub struct StyledEditorState {
pub mode: Mode, pub field_id: FieldId,
pub initial_text: String, pub state: EditorMode,
pub current_mode: StyledCheckboxState,
pub user_mode: TextEditorMode,
} }
impl StyledEditorState { impl StyledEditorState {
#[inline(always)] pub fn new(field_id: FieldId, user_mode: TextEditorMode, value: &str, html: &str) -> Self {
pub fn new<S: Into<String>>(mode: Mode, text: S) -> Self {
Self { Self {
mode, current_mode: StyledCheckboxState::new(
initial_text: text.into(), field_id.clone(),
match user_mode {
TextEditorMode::RteOnly => TextEditorMode::RteOnly,
TextEditorMode::MdOnly | TextEditorMode::Mixed => TextEditorMode::MdOnly,
}
.into(),
),
state: build_state(user_mode, field_id.clone(), value, html),
field_id,
user_mode,
}
}
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
match msg {
Msg::U32InputChanged(field_id, mode)
if &self.field_id == field_id && self.current_mode.value != *mode =>
{
let mode: TextEditorMode = (*mode).into();
self.state = build_state(mode, self.field_id.clone(), "", "")
}
_ => {}
};
self.current_mode.update(msg);
self.state.update(msg, orders);
}
pub fn set_content(&mut self, text: &str, html: &str) {
match &mut self.state {
EditorMode::Md(md) => {
md.initial_text = text.to_string();
md.html = html.to_string();
}
EditorMode::Rte(rte) => {
rte.value = html.to_string();
}
} }
} }
} }
#[derive(Debug, Clone)] impl EditorMode {
pub struct StyledEditor<'l> { pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
pub id: Option<FieldId>, match self {
pub initial_text: &'l str, EditorMode::Md(state) => state.update(msg),
pub text: &'l str, EditorMode::Rte(state) => state.update(msg, orders),
pub html: &'l str, };
pub mode: Mode,
pub update_event: Ev,
}
impl<'l> Default for StyledEditor<'l> {
#[inline(always)]
fn default() -> Self {
Self {
id: None,
initial_text: "",
text: "",
html: "",
mode: Mode::Editor,
update_event: Ev::Cached,
}
} }
} }
impl<'l> StyledEditor<'l> { /// Build project description text area with styled field wrapper
#[inline(always)] #[inline(always)]
pub fn render(self) -> Node<Msg> { pub fn render_styled_editor(state: &StyledEditorState) -> Node<Msg> {
let StyledEditor { let editor = match &state.state {
id, EditorMode::Md(state) => render_md(state),
initial_text, EditorMode::Rte(state) => render_rte(state),
text: _, };
html, let switcher = render_mode_switcher(state);
mode, div![switcher, editor]
update_event, }
} = self;
let id = id.expect("Styled Editor requires ID"); #[inline(always)]
let on_editor_clicked = click_handler(id.clone(), Mode::Editor); pub fn render_mode_switcher(state: &StyledEditorState) -> Node<Msg> {
let on_view_clicked = click_handler(id.clone(), Mode::View); match state.user_mode {
TextEditorMode::Mixed => StyledCheckbox {
let editor_id = format!("editor-{}", id); options: Some(
let view_id = format!("view-{}", id); vec![TextEditorMode::MdOnly, TextEditorMode::RteOnly]
let name = format!("styled-editor-{}", id); .into_iter()
.map(|tem| editor_mode_checkbox_option(tem, &state.current_mode)),
let text_area = StyledTextarea { ),
id: Some(id), class_list: "textEditorModeSwitcher",
height: 40,
value: initial_text,
update_event,
..Default::default()
} }
.render(); .render(),
_ => Node::Empty,
div![
C!["styledEditor"],
label![
C!["navbar viewTab", IF![mode == Mode::View => "activeTab"]],
attrs![At::For => view_id.as_str()],
"View",
on_view_clicked
],
label![
C!["navbar editorTab", IF![mode == Mode::Editor => "activeTab"]],
attrs![At::For => editor_id.as_str()],
"Editor",
on_editor_clicked
],
seed::input![
id![editor_id.as_str()],
C!["editorRadio"],
attrs![At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
],
text_area,
seed::input![
id![view_id.as_str()],
C!["viewRadio"],
attrs![ At::Type => "radio"; At::Name => name.as_str();],
IF![mode == Mode::View => attrs![At::Checked => true]]
],
div![
C!["view"],
IF![mode == Mode::Editor => empty![]],
IF![mode == Mode::View => raw![html]],
],
]
} }
} }
#[inline(always)] #[inline(always)]
fn click_handler(field_id: FieldId, new_mode: Mode) -> EventHandler<Msg> { fn editor_mode_checkbox_option<'l>(
mouse_ev(Ev::Click, move |ev| { tem: TextEditorMode,
ev.stop_propagation(); state: &StyledCheckboxState,
Msg::ModalChanged(FieldChange::TabChanged(field_id, new_mode)) ) -> ChildBuilder<'l> {
}) let value: u32 = tem.into();
ChildBuilder {
field_id: state.field_id.clone(),
selected: state.value == value,
label: match tem {
TextEditorMode::MdOnly => "Simple",
TextEditorMode::RteOnly => "Rich Text Editor",
TextEditorMode::Mixed => "Editor with possibility to switch between modes",
},
class_list: tem.to_str(),
value,
icon: match tem {
TextEditorMode::MdOnly => StyledIcon::from(Icon::MdEditor).render(),
TextEditorMode::RteOnly => StyledIcon::from(Icon::RteEditor).render(),
TextEditorMode::Mixed => Node::Empty,
},
..Default::default()
}
}
/// Build project description text area with styled field wrapper
#[inline(always)]
fn render_md(state: &StyledMdEditorState) -> Node<Msg> {
StyledMdEditor {
id: Some(state.id.clone()),
initial_text: state.initial_text.as_str(),
text: state.initial_text.as_str(),
html: state.html.as_str(),
mode: state.mode.clone(),
update_event: Ev::Change,
}
.render()
}
/// Build project description text area with styled field wrapper
#[inline(always)]
fn render_rte(state: &StyledRteState) -> Node<Msg> {
let id = state.field_id.clone();
StyledRte {
field_id: id,
table_tooltip: Some(&state.table_tooltip),
code_tooltip: Some(&state.code_tooltip),
identifier: Some(state.identifier),
}
.render()
}
fn build_state(
user_mode: TextEditorMode,
field_id: FieldId,
value: &str,
html: &str,
) -> EditorMode {
match user_mode {
TextEditorMode::RteOnly => build_state_rte(field_id),
TextEditorMode::MdOnly | TextEditorMode::Mixed => build_state_md(field_id, value, html),
}
}
fn build_state_rte(field_id: FieldId) -> EditorMode {
EditorMode::Rte(StyledRteState::new(field_id))
}
fn build_state_md(field_id: FieldId, text: &str, html: &str) -> EditorMode {
EditorMode::Md(StyledMdEditorState::new(
field_id,
MdEditorMode::View,
text,
html,
))
} }

View File

@ -96,6 +96,9 @@ pub enum Icon {
TextWidth, TextWidth,
Underline, Underline,
Undo, Undo,
MdEditor,
RteEditor,
} }
impl Icon { impl Icon {
@ -197,6 +200,10 @@ impl Icon {
Icon::DoubleLeft => "double-left", Icon::DoubleLeft => "double-left",
Icon::DoubleRight => "double-right", Icon::DoubleRight => "double-right",
// editor
Icon::MdEditor => "icofont-brand-compaq",
Icon::RteEditor => "icofont-compass-alt-4",
} }
} }
} }

View File

@ -117,6 +117,7 @@ pub struct StyledInput<'l, 'm: 'l> {
pub variant: InputVariant, pub variant: InputVariant,
pub auto_focus: bool, pub auto_focus: bool,
pub input_handlers: Vec<EventHandler<Msg>>, pub input_handlers: Vec<EventHandler<Msg>>,
pub err_msg: &'static str,
} }
impl<'l, 'm: 'l> Default for StyledInput<'l, 'm> { impl<'l, 'm: 'l> Default for StyledInput<'l, 'm> {
@ -132,24 +133,7 @@ impl<'l, 'm: 'l> Default for StyledInput<'l, 'm> {
variant: InputVariant::Normal, variant: InputVariant::Normal,
auto_focus: false, auto_focus: false,
input_handlers: vec![], input_handlers: vec![],
} err_msg: "",
}
}
impl<'l, 'm: 'l> StyledInput<'l, 'm> {
#[inline(always)]
pub fn new_with_id_and_value_and_valid(id: FieldId, value: &'m str, valid: bool) -> Self {
Self {
id: Some(id),
icon: None,
valid,
value,
input_type: None,
input_class_list: "",
wrapper_class_list: "",
variant: InputVariant::Normal,
auto_focus: false,
input_handlers: vec![],
} }
} }
} }
@ -168,6 +152,7 @@ impl<'l, 'm: 'l> StyledInput<'l, 'm> {
variant, variant,
auto_focus, auto_focus,
input_handlers, input_handlers,
err_msg,
} = self; } = self;
let id = id.expect("Input id is required"); let id = id.expect("Input id is required");
@ -209,6 +194,7 @@ impl<'l, 'm: 'l> StyledInput<'l, 'm> {
on_change, on_change,
input_handlers, input_handlers,
], ],
IF![!valid => div![C!["error"], err_msg]]
] ]
} }
} }

View File

@ -0,0 +1,150 @@
use seed::prelude::*;
use seed::*;
use crate::components::styled_textarea::StyledTextarea;
use crate::{FieldChange, FieldId, Msg};
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
#[repr(C)]
pub enum MdEditorMode {
Editor,
View,
}
#[derive(Debug, Clone, PartialOrd, PartialEq)]
pub struct StyledMdEditorState {
pub id: FieldId,
pub mode: MdEditorMode,
pub initial_text: String,
pub html: String,
}
impl StyledMdEditorState {
#[inline(always)]
pub fn new<Text: Into<String>, Html: Into<String>>(
id: FieldId,
mode: MdEditorMode,
text: Text,
html: Html,
) -> Self {
Self {
id,
mode,
initial_text: text.into(),
html: html.into(),
}
}
pub fn update(&mut self, msg: &Msg) {
match msg {
Msg::ModalChanged(FieldChange::TabChanged(field_id, new_mode))
if &self.id == field_id =>
{
self.mode = new_mode.clone();
}
_ => {}
};
}
}
#[derive(Debug, Clone)]
pub struct StyledMdEditor<'l> {
pub id: Option<FieldId>,
pub initial_text: &'l str,
pub text: &'l str,
pub html: &'l str,
pub mode: MdEditorMode,
pub update_event: Ev,
}
impl<'l> Default for StyledMdEditor<'l> {
#[inline(always)]
fn default() -> Self {
Self {
id: None,
initial_text: "",
text: "",
html: "",
mode: MdEditorMode::Editor,
update_event: Ev::Cached,
}
}
}
impl<'l> StyledMdEditor<'l> {
#[inline(always)]
pub fn render(self) -> Node<Msg> {
let StyledMdEditor {
id,
initial_text,
text: _,
html,
mode,
update_event,
} = self;
let id = id.expect("Styled Editor requires ID");
let on_editor_clicked = click_handler(id.clone(), MdEditorMode::Editor);
let on_view_clicked = click_handler(id.clone(), MdEditorMode::View);
let editor_id = format!("editor-{}", id);
let view_id = format!("view-{}", id);
let name = format!("styled-editor-{}", id);
let text_area = StyledTextarea {
id: Some(id),
height: 40,
value: initial_text,
update_event,
..Default::default()
}
.render();
div![
C!["styledEditor"],
label![
C![
"navbar viewTab",
IF![mode == MdEditorMode::View => "activeTab"]
],
attrs![At::For => view_id.as_str()],
"View",
on_view_clicked
],
label![
C![
"navbar editorTab",
IF![mode == MdEditorMode::Editor => "activeTab"]
],
attrs![At::For => editor_id.as_str()],
"Editor",
on_editor_clicked
],
seed::input![
id![editor_id.as_str()],
C!["editorRadio"],
attrs![At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
],
text_area,
seed::input![
id![view_id.as_str()],
C!["viewRadio"],
attrs![ At::Type => "radio"; At::Name => name.as_str();],
IF![mode == MdEditorMode::View => attrs![At::Checked => true]]
],
div![
C!["view"],
IF![mode == MdEditorMode::Editor => empty![]],
IF![mode == MdEditorMode::View => raw![html]],
],
]
}
}
#[inline(always)]
fn click_handler(field_id: FieldId, new_mode: MdEditorMode) -> EventHandler<Msg> {
mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
Msg::ModalChanged(FieldChange::TabChanged(field_id, new_mode))
})
}

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ impl<'l> Default for StyledTextarea<'l> {
max_height: 0, max_height: 0,
value: "", value: "",
class_list: "", class_list: "",
update_event: Ev::Cached, update_event: Ev::Change,
placeholder: "", placeholder: "",
disable_auto_resize: false, disable_auto_resize: false,
} }
@ -67,24 +67,25 @@ impl<'l> StyledTextarea<'l> {
)); ));
} }
let handler_disable_auto_resize = disable_auto_resize; // let handler_disable_auto_resize = disable_auto_resize;
let resize_handler = ev(Ev::KeyUp, move |event| { // let resize_handler = ev(Ev::KeyUp, move |event| {
event.stop_propagation(); // event.stop_propagation();
if handler_disable_auto_resize { // if handler_disable_auto_resize {
return None as Option<Msg>; // return None as Option<Msg>;
} // }
//
let target = event.target().unwrap(); // let target = event.target().unwrap();
let textarea = seed::to_textarea(&target); // let textarea = seed::to_textarea(&target);
let value = textarea.value(); // let value = textarea.value();
let min_height = // let min_height =
get_min_height(value.as_str(), height as f64, handler_disable_auto_resize); // get_min_height(value.as_str(), height as f64,
// handler_disable_auto_resize);
textarea //
.style() // textarea
.set_css_text(format!("height: {min_height}px", min_height = min_height).as_str()); // .style()
None as Option<Msg> // .set_css_text(format!("height: {min_height}px", min_height =
}); // min_height).as_str()); None as Option<Msg>
// });
let handler_disable_auto_resize = disable_auto_resize; let handler_disable_auto_resize = disable_auto_resize;
let text_input_handler = { let text_input_handler = {
@ -122,10 +123,11 @@ impl<'l> StyledTextarea<'l> {
At::AutoFocus => "true"; At::AutoFocus => "true";
At::Style => style_list.join(";"); At::Style => style_list.join(";");
At::Placeholder => placeholder; At::Placeholder => placeholder;
At::Rows => if disable_auto_resize { "5" } else { "auto" } At::Rows => if disable_auto_resize { "5" } else { "auto" },
At::Data => height
], ],
value, value,
resize_handler, // resize_handler,
text_input_handler, text_input_handler,
] ]
] ]
@ -139,6 +141,18 @@ const PADDING_TOP_BOTTOM: f64 = 17f64;
const BORDER_TOP_BOTTOM: f64 = 2f64; const BORDER_TOP_BOTTOM: f64 = 2f64;
const ADDITIONAL_HEIGHT: f64 = PADDING_TOP_BOTTOM + BORDER_TOP_BOTTOM; const ADDITIONAL_HEIGHT: f64 = PADDING_TOP_BOTTOM + BORDER_TOP_BOTTOM;
pub fn handle_resize(target: &web_sys::Element) -> Option<Msg> {
let height: usize = target.get_attribute("data")?.parse().ok()?;
let textarea = seed::to_textarea(target);
let value = textarea.value();
let min_height = get_min_height(value.as_str(), height as f64, false);
textarea
.style()
.set_css_text(format!("height: {min_height}px", min_height = min_height).as_str());
None
}
fn get_min_height(value: &str, min_height: f64, disable_auto_resize: bool) -> f64 { fn get_min_height(value: &str, min_height: f64, disable_auto_resize: bool) -> f64 {
if disable_auto_resize { if disable_auto_resize {
return min_height; return min_height;

View File

@ -0,0 +1,22 @@
use seed::prelude::*;
use seed::*;
use crate::model::Model;
use crate::{BuildMsg, Msg};
pub fn styled_tip<B>(letter: char, model: &Model, builder: B) -> Node<Msg>
where
B: BuildMsg + 'static,
{
model
.key_triggers
.borrow_mut()
.insert(letter, Box::new(builder));
div![
C!["proTip"],
strong![C!["strong"], "Pro tip: "],
"press ",
span![C!["tipLetter", letter.to_string()], letter.to_string()],
" to comment"
]
}

View File

@ -86,6 +86,7 @@ impl ButtonId {
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum FieldId { pub enum FieldId {
NoField,
SignIn(SignInFieldId), SignIn(SignInFieldId),
SignUp(SignUpFieldId), SignUp(SignUpFieldId),
Invite(InviteFieldId), Invite(InviteFieldId),
@ -104,115 +105,103 @@ pub enum FieldId {
Rte(RteField), Rte(RteField),
} }
impl std::fmt::Display for FieldId { impl FieldId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { pub fn to_str(&self) -> &'static str {
match self { match self {
FieldId::NoField => "",
FieldId::EditIssueModal(sub) => match sub { FieldId::EditIssueModal(sub) => match sub {
EditIssueModalSection::Issue(IssueFieldId::Type) => { EditIssueModalSection::Issue(IssueFieldId::Type) => "issueTypeEditModalTop",
f.write_str("issueTypeEditModalTop") EditIssueModalSection::Issue(IssueFieldId::Title) => "titleIssueEditModal",
}
EditIssueModalSection::Issue(IssueFieldId::Title) => {
f.write_str("titleIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::Description) => { EditIssueModalSection::Issue(IssueFieldId::Description) => {
f.write_str("descriptionIssueEditModal") "descriptionIssueEditModal"
}
EditIssueModalSection::Issue(IssueFieldId::IssueStatusId) => {
f.write_str("statusIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::Assignees) => {
f.write_str("assigneesIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::Reporter) => {
f.write_str("reporterIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::Priority) => {
f.write_str("priorityIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::Estimate) => {
f.write_str("estimateIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::TimeSpent) => {
f.write_str("timeSpendIssueEditModal")
} }
EditIssueModalSection::Issue(IssueFieldId::IssueStatusId) => "statusIssueEditModal",
EditIssueModalSection::Issue(IssueFieldId::Assignees) => "assigneesIssueEditModal",
EditIssueModalSection::Issue(IssueFieldId::Reporter) => "reporterIssueEditModal",
EditIssueModalSection::Issue(IssueFieldId::Priority) => "priorityIssueEditModal",
EditIssueModalSection::Issue(IssueFieldId::Estimate) => "estimateIssueEditModal",
EditIssueModalSection::Issue(IssueFieldId::TimeSpent) => "timeSpendIssueEditModal",
EditIssueModalSection::Issue(IssueFieldId::TimeRemaining) => { EditIssueModalSection::Issue(IssueFieldId::TimeRemaining) => {
f.write_str("timeRemainingIssueEditModal") "timeRemainingIssueEditModal"
}
EditIssueModalSection::Comment(CommentFieldId::Body) => {
f.write_str("editIssue-commentBody")
} }
EditIssueModalSection::Comment(CommentFieldId::Body) => "editIssue-commentBody",
EditIssueModalSection::Issue(IssueFieldId::ListPosition) => { EditIssueModalSection::Issue(IssueFieldId::ListPosition) => {
f.write_str("editIssue-listPosition") "editIssue-listPosition"
}
EditIssueModalSection::Issue(IssueFieldId::EpicName) => {
f.write_str("editIssue-epicName")
} }
EditIssueModalSection::Issue(IssueFieldId::EpicName) => "editIssue-epicName",
EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt) => { EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt) => {
f.write_str("editIssue-epicStartsAt") "editIssue-epicStartsAt"
}
EditIssueModalSection::Issue(IssueFieldId::EpicEndsAt) => {
f.write_str("editIssue-epicEndsAt")
} }
EditIssueModalSection::Issue(IssueFieldId::EpicEndsAt) => "editIssue-epicEndsAt",
}, },
FieldId::AddIssueModal(sub) => match sub { FieldId::AddIssueModal(sub) => match sub {
IssueFieldId::Type => f.write_str("issueTypeAddIssueModal"), IssueFieldId::Type => "issueTypeAddIssueModal",
IssueFieldId::Title => f.write_str("summaryAddIssueModal"), IssueFieldId::Title => "summaryAddIssueModal",
IssueFieldId::Description => f.write_str("descriptionAddIssueModal"), IssueFieldId::Description => "descriptionAddIssueModal",
IssueFieldId::Reporter => f.write_str("reporterAddIssueModal"), IssueFieldId::Reporter => "reporterAddIssueModal",
IssueFieldId::Assignees => f.write_str("assigneesAddIssueModal"), IssueFieldId::Assignees => "assigneesAddIssueModal",
IssueFieldId::Priority => f.write_str("issuePriorityAddIssueModal"), IssueFieldId::Priority => "issuePriorityAddIssueModal",
IssueFieldId::IssueStatusId => f.write_str("addIssueModal-status"), IssueFieldId::IssueStatusId => "addIssueModal-status",
IssueFieldId::Estimate => f.write_str("addIssueModal-estimate"), IssueFieldId::Estimate => "addIssueModal-estimate",
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"), IssueFieldId::TimeSpent => "addIssueModal-timeSpend",
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"), IssueFieldId::TimeRemaining => "addIssueModal-timeRemaining",
IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"), IssueFieldId::ListPosition => "addIssueModal-listPosition",
IssueFieldId::EpicName => f.write_str("addIssueModal-epicName"), IssueFieldId::EpicName => "addIssueModal-epicName",
IssueFieldId::EpicStartsAt => f.write_str("addIssueModal-epicStartsAt"), IssueFieldId::EpicStartsAt => "addIssueModal-epicStartsAt",
IssueFieldId::EpicEndsAt => f.write_str("addIssueModal-epicEndsAt"), IssueFieldId::EpicEndsAt => "addIssueModal-epicEndsAt",
}, },
FieldId::TextFilterBoard => f.write_str("textFilterBoard"), FieldId::TextFilterBoard => "textFilterBoard",
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"), FieldId::CopyButtonLabel => "copyButtonLabel",
FieldId::ProjectSettings(sub) => match sub { FieldId::ProjectSettings(sub) => match sub {
ProjectFieldId::Name => f.write_str("projectSettings-name"), ProjectFieldId::Name => "projectSettings-name",
ProjectFieldId::Url => f.write_str("projectSettings-url"), ProjectFieldId::Url => "projectSettings-url",
ProjectFieldId::Description => f.write_str("projectSettings-description"), ProjectFieldId::Description => "projectSettings-description",
ProjectFieldId::Category => f.write_str("projectSettings-category"), ProjectFieldId::Category => "projectSettings-category",
ProjectFieldId::TimeTracking => f.write_str("projectSettings-timeTracking"), ProjectFieldId::TimeTracking => "projectSettings-timeTracking",
ProjectFieldId::IssueStatusName => f.write_str("projectSettings-issueStatusName"), ProjectFieldId::IssueStatusName => "projectSettings-issueStatusName",
ProjectFieldId::DescriptionMode => "projectSettings-descriptionMode",
}, },
FieldId::SignIn(sub) => match sub { FieldId::SignIn(sub) => match sub {
SignInFieldId::Email => f.write_str("login-email"), SignInFieldId::Email => "login-email",
SignInFieldId::Username => f.write_str("login-username"), SignInFieldId::Username => "login-username",
SignInFieldId::Token => f.write_str("login-token"), SignInFieldId::Token => "login-token",
}, },
FieldId::SignUp(sub) => match sub { FieldId::SignUp(sub) => match sub {
SignUpFieldId::Username => f.write_str("signUp-email"), SignUpFieldId::Username => "signUp-email",
SignUpFieldId::Email => f.write_str("signUp-username"), SignUpFieldId::Email => "signUp-username",
}, },
FieldId::Invite(sub) => match sub { FieldId::Invite(sub) => match sub {
InviteFieldId::Token => f.write_str("invite-token"), InviteFieldId::Token => "invite-token",
}, },
FieldId::Users(sub) => match sub { FieldId::Users(sub) => match sub {
UsersFieldId::Username => f.write_str("users-username"), UsersFieldId::Username => "users-username",
UsersFieldId::Email => f.write_str("users-email"), UsersFieldId::Email => "users-email",
UsersFieldId::UserRole => f.write_str("users-userRole"), UsersFieldId::UserRole => "users-userRole",
UsersFieldId::Avatar => f.write_str("users-avatar"), UsersFieldId::Avatar => "users-avatar",
UsersFieldId::CurrentProject => f.write_str("users-currentProject"), UsersFieldId::CurrentProject => "users-currentProject",
UsersFieldId::TextEditorMode => "users-textEditorMode",
}, },
FieldId::Profile(sub) => match sub { FieldId::Profile(sub) => match sub {
UsersFieldId::Username => f.write_str("profile-username"), UsersFieldId::Username => "profile-username",
UsersFieldId::Email => f.write_str("profile-email"), UsersFieldId::Email => "profile-email",
UsersFieldId::UserRole => f.write_str("profile-userRole"), UsersFieldId::UserRole => "profile-userRole",
UsersFieldId::Avatar => f.write_str("profile-avatar"), UsersFieldId::Avatar => "profile-avatar",
UsersFieldId::CurrentProject => f.write_str("profile-currentProject"), UsersFieldId::CurrentProject => "profile-currentProject",
UsersFieldId::TextEditorMode => "profile-textEditorMode",
}, },
FieldId::EditEpic(sub) => match sub { FieldId::EditEpic(sub) => match sub {
EpicFieldId::Name => f.write_str("epicEpic-name"), EpicFieldId::Name => "epicEpic-name",
EpicFieldId::StartsAt => f.write_str("epicEpic-startsAt"), EpicFieldId::StartsAt => "epicEpic-startsAt",
EpicFieldId::EndsAt => f.write_str("epicEpic-endsAt"), EpicFieldId::EndsAt => "epicEpic-endsAt",
EpicFieldId::TransformInto => f.write_str("epicEpic-transformInto"), EpicFieldId::TransformInto => "epicEpic-transformInto",
}, },
FieldId::Rte(..) => f.write_str("rte"), FieldId::Rte(..) => "rte",
} }
} }
} }
impl std::fmt::Display for FieldId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.to_str())
}
}

View File

@ -6,13 +6,14 @@ pub use fields::*;
pub use images::*; pub use images::*;
use jirs_data::*; use jirs_data::*;
use seed::prelude::*; use seed::prelude::*;
use seed::*;
use web_sys::File; use web_sys::File;
use crate::components::styled_date_time_input::StyledDateTimeChanged; use crate::components::styled_date_time_input::StyledDateTimeChanged;
use crate::components::styled_rte::RteMsg;
use crate::components::styled_select::StyledSelectChanged; use crate::components::styled_select::StyledSelectChanged;
use crate::components::styled_tooltip; use crate::components::styled_tooltip;
use crate::components::styled_tooltip::{TooltipVariant as StyledTooltip, TooltipVariant}; use crate::components::styled_tooltip::{TooltipVariant as StyledTooltip, TooltipVariant};
use crate::modals::DebugMsg;
use crate::model::{ModalType, Model, Page}; use crate::model::{ModalType, Model, Page};
use crate::shared::{go_to_board, go_to_login}; use crate::shared::{go_to_board, go_to_login};
use crate::ws::{flush_queue, open_socket, read_incoming, send_ws_msg}; use crate::ws::{flush_queue, open_socket, read_incoming, send_ws_msg};
@ -42,6 +43,7 @@ pub enum ResourceKind {
Epic, Epic,
Project, Project,
User, User,
UserSetting,
UserProject, UserProject,
Message, Message,
Comment, Comment,
@ -58,17 +60,19 @@ pub enum OperationKind {
SingleModified, SingleModified,
} }
pub trait BuildMsg: std::fmt::Debug {
fn build(&self) -> Msg;
}
#[derive(Debug)] #[derive(Debug)]
pub enum Msg { pub enum Msg {
GlobalKeyDown { #[cfg(debug_assertions)]
key: String, Debug(DebugMsg),
shift: bool,
ctrl: bool,
alt: bool,
},
PageChanged(PageChanged), PageChanged(PageChanged),
ChangePage(model::Page), ChangePage(model::Page),
Rte(FieldId, RteMsg),
UserChanged(Option<User>), UserChanged(Option<User>),
ProjectChanged(Option<Project>), ProjectChanged(Option<Project>),
@ -203,7 +207,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
}; };
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
log!(msg); log::info!("msg {:?}", msg);
} }
match &msg { match &msg {
@ -232,7 +236,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
_ => (), _ => (),
} }
{ if !matches!(model.page, Page::SignIn | Page::SignUp) {
use crate::shared::{aside, navbar_left}; use crate::shared::{aside, navbar_left};
aside::update(&msg, model, orders); aside::update(&msg, model, orders);
navbar_left::update(&msg, model, orders); navbar_left::update(&msg, model, orders);
@ -254,11 +258,13 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
Page::Reports => pages::reports_page::update(msg, model, orders), Page::Reports => pages::reports_page::update(msg, model, orders),
} }
if cfg!(features = "print-model") { if cfg!(features = "print-model") {
log!(model); log::debug!("{:?}", model);
} }
} }
fn view(model: &model::Model) -> Node<Msg> { fn view(model: &model::Model) -> Node<Msg> {
model.key_triggers.borrow_mut().clear();
match model.page { match model.page {
Page::Project Page::Project
| Page::AddIssue | Page::AddIssue
@ -275,6 +281,7 @@ fn view(model: &model::Model) -> Node<Msg> {
} }
} }
#[inline(always)]
fn resolve_page(url: Url) -> Option<Page> { fn resolve_page(url: Url) -> Option<Page> {
if url.path().is_empty() { if url.path().is_empty() {
return Some(Page::Project); return Some(Page::Project);
@ -310,49 +317,102 @@ fn resolve_page(url: Url) -> Option<Page> {
#[wasm_bindgen] #[wasm_bindgen]
pub fn render() { pub fn render() {
let app = seed::App::start("app", init, update, view); let app = seed::App::start("app", init, update, view);
wasm_logger::init(wasm_logger::Config::default());
{ #[cfg(debug_assertions)]
let app_clone = app.clone(); crate::shared::on_event::keydown(move |ev| {
let on_key_down = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { let app = app.clone();
let event: web_sys::KeyboardEvent = event.unchecked_into(); let event = seed::to_keyboard_event(&ev);
let tag_name: String = seed::document() if seed::document()
.active_element() .active_element()
.map(|el| el.tag_name()) .map(|el| el.tag_name() != "BODY")
.unwrap_or_default(); .unwrap_or(true)
|| !event.shift_key()
|| !event.ctrl_key()
{
return;
}
let key = match tag_name.to_lowercase().as_str() { let key: String = event.key();
"input" | "textarea" => return, match key.as_str() {
_ => event.key(), ">" => app.update(Msg::Debug(DebugMsg::Console)),
}; "?" => app.update(Msg::Debug(DebugMsg::Modal)),
_ => {}
};
});
let msg = Msg::GlobalKeyDown { crate::shared::on_event::keydown(move |_ev| {
key, let element = match seed::document().active_element() {
shift: event.shift_key(), Some(el) => el,
ctrl: event.ctrl_key(), _ => return,
alt: event.alt_key(), };
}; let class_list = element.class_name();
app_clone.update(msg); if !class_list.contains("textAreaInput") {
}) as Box<dyn FnMut(_)>); return;
seed::body() }
.add_event_listener_with_callback("keyup", on_key_down.as_ref().unchecked_ref()) if element.get_attribute("rows").as_deref() != Some("auto") {
.expect("Failed to mount global key handler"); return;
on_key_down.forget(); }
} crate::components::styled_textarea::handle_resize(&element);
});
} }
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model { fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
let sender = orders.msg_sender();
let page = resolve_page(url).unwrap_or(Page::Project);
let mut model = Model::new( let mut model = Model::new(
location::host_url().to_string(), location::host_url().to_string(),
location::ws_url().to_string(), location::ws_url().to_string(),
page,
); );
model.page = resolve_page(url).unwrap_or(Page::Project); let key_triggers = model.key_triggers.clone();
let sender_clone = sender.clone();
crate::shared::on_event::keypress(move |ev| {
let sender = sender_clone.clone();
let key_triggers = key_triggers.clone();
let event = seed::to_keyboard_event(&ev);
if seed::document()
.active_element()
.map(|el| el.tag_name() != "BODY")
.unwrap_or(true)
{
return;
}
ev.prevent_default();
ev.stop_propagation();
let key: String = event.key();
let t = key_triggers.borrow();
if let Some(b) = key.chars().next().and_then(|c| t.get(&c)) {
let msg = b.build();
sender.clone()(Some(msg));
}
});
{
let sender_clone = sender.clone();
let id = FieldId::ProjectSettings(ProjectFieldId::Description);
model
.distinct_key_up
.keyup_wih_reset(id.to_str(), 20, move |ev| {
let sender = sender_clone.clone();
let key_ev = seed::to_keyboard_event(&ev);
let target = key_ev.target().unwrap();
let el = seed::to_html_el(&target);
let value = el.inner_html();
sender.clone()(Some(Msg::StrInputChanged(id.clone(), value)));
});
}
open_socket(&mut model, orders); open_socket(&mut model, orders);
model model
} }
#[inline] #[inline(always)]
fn authorize_or_redirect(model: &mut Model, orders: &mut impl Orders<Msg>) { fn authorize_or_redirect(model: &mut Model, orders: &mut impl Orders<Msg>) {
let pathname = seed::document().location().unwrap().pathname().unwrap(); let pathname = seed::document().location().unwrap().pathname().unwrap();
match crate::shared::read_auth_token() { match crate::shared::read_auth_token() {

View File

@ -3,6 +3,7 @@ use seed::prelude::Orders;
use crate::components::styled_checkbox::StyledCheckboxState; use crate::components::styled_checkbox::StyledCheckboxState;
use crate::components::styled_input::*; use crate::components::styled_input::*;
use crate::styled_date_time_input::StyledDateTimeInputState;
use crate::{model, FieldId, Msg}; use crate::{model, FieldId, Msg};
#[derive(Debug)] #[derive(Debug)]
@ -11,15 +12,19 @@ pub struct Model {
pub related_issues: Vec<IssueId>, pub related_issues: Vec<IssueId>,
pub name: StyledInputState, pub name: StyledInputState,
pub transform_into: StyledCheckboxState, pub transform_into: StyledCheckboxState,
pub starts_at: StyledDateTimeInputState,
pub ends_at: StyledDateTimeInputState,
} }
impl Model { impl Model {
pub fn new(epic_id: i32, model: &mut model::Model) -> Self { pub fn new(epic_id: i32, model: &mut model::Model) -> Self {
let name = model let (name, starts_at, ends_at) = {
.epics_by_id if let Some(epic) = model.epics_by_id.get(&epic_id) {
.get(&epic_id) (epic.name.as_str(), epic.starts_at, epic.ends_at)
.map(|epic| epic.name.as_str()) } else {
.unwrap_or_default(); ("", None, None)
}
};
let related_issues = model let related_issues = model
.issues() .issues()
@ -40,11 +45,18 @@ impl Model {
FieldId::EditEpic(EpicFieldId::TransformInto), FieldId::EditEpic(EpicFieldId::TransformInto),
0, 0,
), ),
starts_at: StyledDateTimeInputState::new(
FieldId::EditEpic(EpicFieldId::StartsAt),
starts_at,
),
ends_at: StyledDateTimeInputState::new(FieldId::EditEpic(EpicFieldId::EndsAt), ends_at),
} }
} }
pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<Msg>) { pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
self.name.update(msg); self.name.update(msg);
self.starts_at.update(msg, orders);
self.ends_at.update(msg, orders);
self.transform_into.update(msg); self.transform_into.update(msg);
} }
} }

View File

@ -1,6 +1,7 @@
use jirs_data::{EpicFieldId, IssueType, WsMsg}; use jirs_data::{EpicFieldId, IssueType, WsMsg};
use seed::prelude::*; use seed::prelude::*;
use crate::components::styled_date_time_input::StyledDateTimeChanged;
use crate::{send_ws_msg, FieldId, Msg, OperationKind, ResourceKind}; use crate::{send_ws_msg, FieldId, Msg, OperationKind, ResourceKind};
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>) {
@ -34,7 +35,29 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
Msg::StrInputChanged(FieldId::EditEpic(EpicFieldId::Name), s) => { Msg::StrInputChanged(FieldId::EditEpic(EpicFieldId::Name), s) => {
let epic_id = modal.epic_id; let epic_id = modal.epic_id;
send_ws_msg( send_ws_msg(
WsMsg::EpicUpdate(epic_id, s.to_string()), WsMsg::EpicUpdateName(epic_id, s.to_string()),
model.ws.as_ref(),
orders,
);
}
Msg::StyledDateTimeInputChanged(
FieldId::EditEpic(EpicFieldId::StartsAt),
StyledDateTimeChanged::DayChanged(Some(date)),
) => {
let epic_id = modal.epic_id;
send_ws_msg(
WsMsg::EpicUpdateStartsAt(epic_id, Some(*date)),
model.ws.as_ref(),
orders,
);
}
Msg::StyledDateTimeInputChanged(
FieldId::EditEpic(EpicFieldId::EndsAt),
StyledDateTimeChanged::DayChanged(Some(date)),
) => {
let epic_id = modal.epic_id;
send_ws_msg(
WsMsg::EpicUpdateEndsAt(epic_id, Some(*date)),
model.ws.as_ref(), model.ws.as_ref(),
orders, orders,
); );

View File

@ -4,10 +4,12 @@ use seed::*;
use crate::components::styled_button::*; use crate::components::styled_button::*;
use crate::components::styled_checkbox::*; use crate::components::styled_checkbox::*;
use crate::components::styled_date_time_input::StyledDateTimeInput;
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
use crate::components::styled_input::*; use crate::components::styled_input::*;
use crate::components::styled_modal::*; use crate::components::styled_modal::*;
use crate::modals::epics_edit::Model; use crate::modals::epics_edit::Model;
use crate::styled_field::StyledField;
use crate::{model, FieldId, Msg}; use crate::{model, FieldId, Msg};
pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> { pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
@ -27,6 +29,33 @@ pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
..Default::default() ..Default::default()
} }
.render(); .render();
let starts_at = StyledDateTimeInput {
field_id: modal.starts_at.field_id.clone(),
popup_visible: modal.starts_at.popup_visible,
timestamp: modal.starts_at.timestamp,
}
.render();
let starts_at = StyledField {
input: starts_at,
label: "Starts At",
..Default::default()
}
.render();
let ends_at = StyledDateTimeInput {
field_id: modal.ends_at.field_id.clone(),
popup_visible: modal.ends_at.popup_visible,
timestamp: modal.ends_at.timestamp,
}
.render();
let ends_at = StyledField {
input: ends_at,
label: "Ends At",
..Default::default()
}
.render();
StyledModal { StyledModal {
width: Some(600), width: Some(600),
class_list: "editEpic", class_list: "editEpic",
@ -39,6 +68,8 @@ pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
..Default::default() ..Default::default()
} }
.render(), .render(),
starts_at,
ends_at,
transform, transform,
], ],
..Default::default() ..Default::default()
@ -79,6 +110,7 @@ fn issue_type_select_option<'l>(ty: IssueType, state: &StyledCheckboxState) -> C
value: ty.into(), value: ty.into(),
class_list: ty.to_str(), class_list: ty.to_str(),
selected: value == state.value, selected: value == state.value,
..Default::default()
} }
} }

View File

@ -42,7 +42,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
input: StyledDateTimeInput { input: StyledDateTimeInput {
field_id: FieldId::AddIssueModal(IssueFieldId::EpicStartsAt), field_id: FieldId::AddIssueModal(IssueFieldId::EpicStartsAt),
popup_visible: modal.epic_starts_at_state.popup_visible, popup_visible: modal.epic_starts_at_state.popup_visible,
timestamp: modal.epic_starts_at_state.timestamp.clone(), timestamp: modal.epic_starts_at_state.timestamp,
} }
.render(), .render(),
label: "Starts at", label: "Starts at",
@ -54,7 +54,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
input: StyledDateTimeInput { input: StyledDateTimeInput {
field_id: FieldId::AddIssueModal(IssueFieldId::EpicEndsAt), field_id: FieldId::AddIssueModal(IssueFieldId::EpicEndsAt),
popup_visible: modal.epic_ends_at_state.popup_visible, popup_visible: modal.epic_ends_at_state.popup_visible,
timestamp: modal.epic_ends_at_state.timestamp.clone(), timestamp: modal.epic_ends_at_state.timestamp,
} }
.render(), .render(),
label: "Ends at", label: "Ends at",

View File

@ -1,15 +1,15 @@
use jirs_data::{Issue, IssueFieldId, IssueId, TimeTracking, UpdateIssuePayload}; use jirs_data::{Issue, IssueFieldId, IssueId, TextEditorMode, TimeTracking, UpdateIssuePayload};
use seed::prelude::*; use seed::prelude::*;
use crate::components::styled_date_time_input::StyledDateTimeInputState; use crate::components::styled_date_time_input::StyledDateTimeInputState;
use crate::components::styled_editor::{Mode, StyledEditorState}; use crate::components::styled_editor::StyledEditorState;
use crate::components::styled_input::StyledInputState; use crate::components::styled_input::StyledInputState;
use crate::components::styled_select::StyledSelectState; use crate::components::styled_select::StyledSelectState;
use crate::modals::time_tracking::value_for_time_tracking; use crate::modals::time_tracking::value_for_time_tracking;
use crate::model::{CommentForm, IssueModal}; use crate::model::{CommentForm, IssueModal};
use crate::{EditIssueModalSection, FieldId, Msg}; use crate::{EditIssueModalSection, FieldId, Msg};
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Debug)]
pub struct Model { pub struct Model {
pub id: IssueId, pub id: IssueId,
pub link_copied: bool, pub link_copied: bool,
@ -38,7 +38,7 @@ pub struct Model {
} }
impl Model { impl Model {
pub fn new(issue: &Issue, time_tracking_type: TimeTracking) -> Self { pub fn new(user_mode: TextEditorMode, issue: &Issue, time_tracking_type: TimeTracking) -> Self {
Self { Self {
id: issue.id, id: issue.id,
link_copied: false, link_copied: false,
@ -110,8 +110,10 @@ impl Model {
) )
.with_min(Some(3)), .with_min(Some(3)),
description_state: StyledEditorState::new( description_state: StyledEditorState::new(
Mode::View, FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
issue.description_text.as_deref().unwrap_or(""), user_mode,
issue.description_text.as_deref().unwrap_or_default(),
issue.description.as_deref().unwrap_or_default(),
), ),
comment_form: CommentForm { comment_form: CommentForm {
id: None, id: None,
@ -163,5 +165,6 @@ impl IssueModal for Model {
self.epic_name_state.update(msg, orders); self.epic_name_state.update(msg, orders);
self.title_state.update(msg); self.title_state.update(msg);
self.description_state.update(msg, orders);
} }
} }

View File

@ -17,11 +17,10 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleModified, Some(id)) => { Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleModified, Some(id)) => {
let m = model.issues_by_id.get(id).cloned(); let m = model.issues_by_id.get(id).cloned();
if let Some(issue) = m { if let Some(issue) = m {
modal.description_state.initial_text = issue modal.description_state.set_content(
.description_text issue.description_text.as_deref().unwrap_or_default(),
.as_deref() issue.description.as_deref().unwrap_or_default(),
.unwrap_or_default() );
.to_string();
modal.payload = issue.into(); modal.payload = issue.into();
} }
} }
@ -282,12 +281,6 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
orders, orders,
); );
} }
Msg::ModalChanged(FieldChange::TabChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
mode,
)) => {
modal.description_state.mode = mode.clone();
}
Msg::ModalChanged(FieldChange::ToggleCommentForm( Msg::ModalChanged(FieldChange::ToggleCommentForm(
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
flag, flag,
@ -346,16 +339,6 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
orders.skip().send_msg(Msg::ModalDropped); 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,
)));
}
_ => (), _ => (),
} }
} }

View File

@ -8,32 +8,42 @@ use seed::*;
use crate::components::styled_avatar::StyledAvatar; use crate::components::styled_avatar::StyledAvatar;
use crate::components::styled_button::{ButtonVariant, StyledButton}; use crate::components::styled_button::{ButtonVariant, StyledButton};
use crate::components::styled_editor::StyledEditor; use crate::components::styled_editor::render_styled_editor;
use crate::components::styled_field::StyledField; use crate::components::styled_field::StyledField;
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
use crate::components::styled_input::StyledInput; use crate::components::styled_input::StyledInput;
use crate::components::styled_modal::*; use crate::components::styled_modal::*;
use crate::components::styled_select::{SelectVariant, StyledSelect, StyledSelectState}; use crate::components::styled_select::{SelectVariant, StyledSelect, StyledSelectState};
use crate::components::styled_select_child::StyledSelectOption; use crate::components::styled_select_child::StyledSelectOption;
use crate::components::styled_tip::styled_tip;
use crate::modals::epic_field; use crate::modals::epic_field;
use crate::modals::issues_edit::Model as EditIssueModal; use crate::modals::issues_edit::Model as EditIssueModal;
use crate::modals::time_tracking::time_tracking_field; use crate::modals::time_tracking::time_tracking_field;
use crate::model::{ModalType, Model}; use crate::model::{ModalType, Model};
use crate::shared::tracking_widget::tracking_link; use crate::shared::tracking_widget::tracking_link;
use crate::{EditIssueModalSection, FieldChange, FieldId, Msg}; use crate::{BuildMsg, EditIssueModalSection, FieldChange, FieldId, Msg};
mod comments; mod comments;
#[inline(always)]
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> { pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
model model
.issues_by_id .issues_by_id
.get(&modal.id) .get(&modal.id)
.map(|_issue| { .map(|_issue| {
StyledModal::centered_with_width_and_body(1040, vec![details(model, modal)]).render() StyledModal {
variant: ModalVariant::Center,
width: Some(1014),
with_icon: false,
children: vec![details(model, modal)],
class_list: "",
}
.render()
}) })
.unwrap_or(Node::Empty) .unwrap_or(Node::Empty)
} }
#[inline(always)]
pub fn details(model: &Model, modal: &EditIssueModal) -> Node<Msg> { pub fn details(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
div![ div![
C!["issueDetails"], C!["issueDetails"],
@ -46,6 +56,7 @@ pub fn details(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
] ]
} }
#[inline(always)]
fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node<Msg> { fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal { let EditIssueModal {
id, id,
@ -189,9 +200,9 @@ fn type_select_option<'l>(t: IssueType, text: &'l str) -> StyledSelectOption<'l>
} }
} }
#[inline(always)]
fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> { fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal { let EditIssueModal {
payload,
description_state, description_state,
comment_form, comment_form,
.. ..
@ -209,19 +220,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
} }
.render(); .render();
let description = { let description = render_styled_editor(&description_state);
StyledEditor {
id: Some(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Description,
))),
initial_text: description_state.initial_text.as_str(),
text: description_state.initial_text.as_str(),
html: payload.description.as_deref().unwrap_or_default(),
mode: description_state.mode.clone(),
update_event: Ev::Change,
}
.render()
};
let description_field = StyledField { let description_field = StyledField {
input: description, input: description,
..Default::default() ..Default::default()
@ -265,13 +264,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
div![ div![
C!["right"], C!["right"],
create_comment, create_comment,
div![ styled_tip('m', model, EnableCommentBuilder)
C!["proTip"],
strong![C!["strong"], "Pro tip: "],
"press ",
span![C!["tipLetter"], "M"],
" to comment"
]
] ]
], ],
comments comments
@ -279,6 +272,19 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
] ]
} }
#[derive(Debug)]
pub struct EnableCommentBuilder;
impl BuildMsg for EnableCommentBuilder {
fn build(&self) -> Msg {
Msg::ModalChanged(FieldChange::ToggleCommentForm(
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
true,
))
}
}
#[inline(always)]
fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> { fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal { let EditIssueModal {
payload, payload,
@ -377,7 +383,7 @@ fn priority_select_option<'l>(ip: IssuePriority) -> StyledSelectOption<'l> {
StyledSelectOption { StyledSelectOption {
icon: Some( icon: Some(
StyledIcon { StyledIcon {
icon: ip.clone().into(), icon: ip.into(),
class_list: ip.to_str(), class_list: ip.to_str(),
..Default::default() ..Default::default()
} }

View File

@ -1,12 +1,18 @@
use jirs_data::{CommentId, EpicId, IssueId, IssueStatusId, TimeTracking, WsMsg}; use jirs_data::{CommentId, EpicId, IssueId, IssueStatusId, TimeTracking, WsMsg};
use seed::prelude::*; use seed::prelude::*;
use seed::*;
use crate::model::{ModalType, Model, Page}; use crate::model::{ModalType, Model, Page};
use crate::shared::go_to_board; use crate::shared::go_to_board;
use crate::ws::send_ws_msg; use crate::ws::send_ws_msg;
use crate::{FieldChange, FieldId, Msg, OperationKind, ResourceKind}; use crate::{FieldChange, FieldId, Msg, OperationKind, ResourceKind};
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub enum DebugMsg {
Console,
Modal,
}
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>) {
match msg { match msg {
Msg::ModalDropped if !model.modal_stack().is_empty() => { Msg::ModalDropped if !model.modal_stack().is_empty() => {
@ -39,15 +45,12 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::ChangePage(Page::EditEpic(id)) => push_edit_epic_modal(*id, model, orders), Msg::ChangePage(Page::EditEpic(id)) => push_edit_epic_modal(*id, model, orders),
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq("#") => push_debug_modal(model), Msg::Debug(DebugMsg::Modal) => push_debug_modal(model),
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq(">") => { Msg::Debug(DebugMsg::Console) => {
orders.skip();
log!(model);
}
Msg::GlobalKeyDown { .. } => {
orders.skip(); orders.skip();
log::debug!("{:?}", model);
} }
_ => (), _ => (),
@ -182,7 +185,12 @@ fn push_edit_issue_modal(issue_id: EpicId, model: &mut Model, orders: &mut impl
Some(issue) => issue, Some(issue) => issue,
_ => return, _ => return,
}; };
crate::modals::issues_edit::Model::new(issue, time_tracking_type) let user_mode = model
.user_settings
.as_ref()
.map(|u| u.text_editor_mode)
.unwrap_or_default();
crate::modals::issues_edit::Model::new(user_mode, issue, time_tracking_type)
}; };
send_ws_msg( send_ws_msg(
WsMsg::IssueCommentsLoad(issue_id), WsMsg::IssueCommentsLoad(issue_id),

View File

@ -15,7 +15,7 @@ use crate::pages::reports_page::model::ReportsPage;
use crate::pages::sign_in_page::model::SignInPage; use crate::pages::sign_in_page::model::SignInPage;
use crate::pages::sign_up_page::model::SignUpPage; use crate::pages::sign_up_page::model::SignUpPage;
use crate::pages::users_page::model::UsersPage; use crate::pages::users_page::model::UsersPage;
use crate::Msg; use crate::{BuildMsg, Msg};
pub trait IssueModal { pub trait IssueModal {
fn epic_id_value(&self) -> Option<u32>; fn epic_id_value(&self) -> Option<u32>;
@ -104,6 +104,24 @@ impl Page {
Page::Reports => "/reports".to_string(), Page::Reports => "/reports".to_string(),
} }
} }
pub fn build_content(&self) -> PageContent {
match self {
Page::Project
| Page::DeleteEpic(..)
| Page::EditEpic(..)
| Page::EditIssue(_)
| Page::AddIssue => PageContent::Project(Box::new(ProjectPage::default())),
//
Page::SignIn => PageContent::SignIn(Box::new(SignInPage::default())),
Page::SignUp => PageContent::SignUp(Box::new(SignUpPage::default())),
Page::Invite => PageContent::Invite(Box::new(InvitePage::default())),
Page::Users => PageContent::Users(Box::new(UsersPage::default())),
Page::Reports => PageContent::Reports(Box::new(ReportsPage::default())),
// for those which requires additional data
_ => PageContent::Project(Box::new(ProjectPage::default())),
}
}
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -141,13 +159,13 @@ impl Default for InvitationFormState {
macro_rules! match_page { macro_rules! match_page {
($model: ident, $ty: ident) => { ($model: ident, $ty: ident) => {
match &$model.page_content { match &$model.page_content {
PageContent::$ty(page) => page, crate::model::PageContent::$ty(page) => page,
_ => return, _ => return,
} }
}; };
($model: ident, $ty: ident; Empty) => { ($model: ident, $ty: ident; Empty) => {
match &$model.page_content { match &$model.page_content {
PageContent::$ty(page) => page, crate::model::PageContent::$ty(page) => page,
_ => return Node::Empty, _ => return Node::Empty,
} }
}; };
@ -212,6 +230,9 @@ pub struct Model {
pub users: Vec<User>, pub users: Vec<User>,
pub users_by_id: HashMap<UserId, User>, pub users_by_id: HashMap<UserId, User>,
// user settings
pub user_settings: Option<UserSetting>,
// comments // comments
pub comments: Vec<Comment>, pub comments: Vec<Comment>,
pub comments_by_id: HashMap<CommentId, Comment>, pub comments_by_id: HashMap<CommentId, Comment>,
@ -235,11 +256,14 @@ pub struct Model {
pub epics: Vec<Epic>, pub epics: Vec<Epic>,
pub epics_by_id: HashMap<EpicId, Epic>, pub epics_by_id: HashMap<EpicId, Epic>,
pub key_triggers: std::rc::Rc<std::cell::RefCell<HashMap<char, Box<dyn BuildMsg>>>>,
pub distinct_key_up: crate::shared::on_event::Distinct,
pub show_extras: bool, pub show_extras: bool,
} }
impl Model { impl Model {
pub fn new(host_url: String, ws_url: String) -> Self { pub fn new(host_url: String, ws_url: String, page: Page) -> Self {
Self { Self {
ws: None, ws: None,
ws_queue: vec![], ws_queue: vec![],
@ -249,10 +273,10 @@ impl Model {
project_form: None, project_form: None,
comment_form: None, comment_form: None,
comments_by_project_id: Default::default(), comments_by_project_id: Default::default(),
page: Page::Project, page_content: page.build_content(),
page,
host_url, host_url,
ws_url, ws_url,
page_content: PageContent::Project(Box::new(ProjectPage::default())),
project: None, project: None,
current_user_project: None, current_user_project: None,
about_tooltip_visible: false, about_tooltip_visible: false,
@ -260,6 +284,7 @@ impl Model {
issues: vec![], issues: vec![],
users: vec![], users: vec![],
users_by_id: Default::default(), users_by_id: Default::default(),
user_settings: None,
comments: vec![], comments: vec![],
comments_by_id: Default::default(), comments_by_id: Default::default(),
issue_statuses: vec![], issue_statuses: vec![],
@ -274,6 +299,8 @@ impl Model {
epics_by_id: Default::default(), epics_by_id: Default::default(),
modals_stack: vec![], modals_stack: vec![],
modals: Default::default(), modals: Default::default(),
key_triggers: std::rc::Rc::new(std::cell::RefCell::new(HashMap::with_capacity(20))),
distinct_key_up: crate::shared::on_event::distinct(),
} }
} }

View File

@ -6,7 +6,7 @@ use crate::components::styled_button::{ButtonVariant, StyledButton};
use crate::components::styled_field::StyledField; use crate::components::styled_field::StyledField;
use crate::components::styled_form::StyledForm; use crate::components::styled_form::StyledForm;
use crate::components::styled_input::StyledInput; use crate::components::styled_input::StyledInput;
use crate::model::{Model, PageContent}; use crate::model::Model;
use crate::pages::invite_page::InvitePage; use crate::pages::invite_page::InvitePage;
use crate::shared::outer_layout; use crate::shared::outer_layout;
use crate::validations::is_token; use crate::validations::is_token;

View File

@ -1,5 +1,6 @@
use jirs_data::{ProjectId, User, UsersFieldId}; use jirs_data::{ProjectId, TextEditorMode, User, UsersFieldId};
use crate::components::styled_checkbox::StyledCheckboxState;
use crate::components::styled_image_input::StyledImageInputState; use crate::components::styled_image_input::StyledImageInputState;
use crate::components::styled_input::StyledInputState; use crate::components::styled_input::StyledInputState;
use crate::components::styled_select::StyledSelectState; use crate::components::styled_select::StyledSelectState;
@ -11,10 +12,11 @@ pub struct ProfilePage {
pub email: StyledInputState, pub email: StyledInputState,
pub avatar: StyledImageInputState, pub avatar: StyledImageInputState,
pub current_project: StyledSelectState, pub current_project: StyledSelectState,
pub text_editor_mode: StyledCheckboxState,
} }
impl ProfilePage { impl ProfilePage {
pub fn new(user: &User, project_ids: Vec<ProjectId>) -> Self { pub fn new(user: &User, mode: TextEditorMode, project_ids: Vec<ProjectId>) -> Self {
Self { Self {
name: StyledInputState::new( name: StyledInputState::new(
FieldId::Profile(UsersFieldId::Username), FieldId::Profile(UsersFieldId::Username),
@ -32,6 +34,10 @@ impl ProfilePage {
FieldId::Profile(UsersFieldId::CurrentProject), FieldId::Profile(UsersFieldId::CurrentProject),
project_ids.into_iter().map(|n| n as u32).collect(), project_ids.into_iter().map(|n| n as u32).collect(),
), ),
text_editor_mode: StyledCheckboxState::new(
FieldId::Profile(UsersFieldId::TextEditorMode),
mode.into(),
),
} }
} }
} }

View File

@ -7,7 +7,8 @@ use crate::model::{Model, Page, PageContent};
use crate::pages::profile_page::model::ProfilePage; use crate::pages::profile_page::model::ProfilePage;
use crate::ws::{board_load, send_ws_msg}; use crate::ws::{board_load, send_ws_msg};
use crate::{ use crate::{
FieldId, Msg, OperationKind, PageChanged, ProfilePageChange, ResourceKind, WebSocketChanged, match_page_mut, FieldId, Msg, OperationKind, PageChanged, ProfilePageChange, ResourceKind,
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>) {
@ -20,17 +21,23 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
_ => (), _ => (),
} }
let profile_page = match &mut model.page_content { let profile_page = match_page_mut!(model, Profile);
PageContent::Profile(profile_page) => profile_page,
_ => return,
};
profile_page.name.update(&msg); profile_page.name.update(&msg);
profile_page.email.update(&msg); profile_page.email.update(&msg);
profile_page.avatar.update(&msg); profile_page.avatar.update(&msg);
profile_page.text_editor_mode.update(&msg);
profile_page.current_project.update(&msg, orders); profile_page.current_project.update(&msg, orders);
match msg { match msg {
Msg::ResourceChanged(ResourceKind::UserSetting, OperationKind::SingleModified, _) => {
profile_page.text_editor_mode.value = model
.user_settings
.as_ref()
.map(|us| us.text_editor_mode)
.unwrap_or_default()
.into();
}
Msg::FileInputChanged(FieldId::Profile(UsersFieldId::Avatar), ..) => { Msg::FileInputChanged(FieldId::Profile(UsersFieldId::Avatar), ..) => {
let file = match profile_page.avatar.file.as_ref() { let file = match profile_page.avatar.file.as_ref() {
Some(f) => f, Some(f) => f,
@ -60,6 +67,13 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
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];
} }
Msg::U32InputChanged(FieldId::Profile(UsersFieldId::TextEditorMode), v) => {
send_ws_msg(
WsMsg::UserSettingSetEditorMode(v.into()),
model.ws.as_ref(),
orders,
);
}
Msg::PageChanged(PageChanged::Profile(ProfilePageChange::SubmitForm)) => { Msg::PageChanged(PageChanged::Profile(ProfilePageChange::SubmitForm)) => {
send_ws_msg( send_ws_msg(
WsMsg::ProfileUpdate( WsMsg::ProfileUpdate(
@ -95,13 +109,20 @@ fn build_page_content(model: &mut Model) {
Some(ref user) => user, Some(ref user) => user,
_ => return, _ => return,
}; };
let text_editor_mode = model
.user_settings
.as_ref()
.map(|us| us.text_editor_mode)
.unwrap_or_default();
let project_ids = model
.project
.as_ref()
.map(|p| vec![p.id])
.unwrap_or_default();
model.page_content = PageContent::Profile(Box::new(ProfilePage::new( model.page_content = PageContent::Profile(Box::new(ProfilePage::new(
user, user,
model text_editor_mode,
.project project_ids,
.as_ref()
.map(|p| vec![p.id])
.unwrap_or_default(),
))); )));
} }

View File

@ -5,22 +5,20 @@ use seed::prelude::*;
use seed::*; use seed::*;
use crate::components::styled_button::{ButtonVariant, StyledButton}; use crate::components::styled_button::{ButtonVariant, StyledButton};
use crate::components::styled_checkbox::{ChildBuilder, StyledCheckbox, StyledCheckboxState};
use crate::components::styled_field::StyledField; use crate::components::styled_field::StyledField;
use crate::components::styled_form::StyledForm; use crate::components::styled_form::StyledForm;
use crate::components::styled_image_input::StyledImageInput; use crate::components::styled_image_input::StyledImageInput;
use crate::components::styled_input::{InputVariant, StyledInput}; use crate::components::styled_input::{InputVariant, StyledInput};
use crate::components::styled_select::{SelectVariant, StyledSelect}; use crate::components::styled_select::{SelectVariant, StyledSelect};
use crate::components::styled_select_child::StyledSelectOption; use crate::components::styled_select_child::StyledSelectOption;
use crate::model::{Model, PageContent}; use crate::model::Model;
use crate::pages::profile_page::model::ProfilePage; use crate::pages::profile_page::model::ProfilePage;
use crate::shared::inner_layout; use crate::shared::inner_layout;
use crate::{FieldId, Msg, PageChanged, ProfilePageChange}; use crate::{match_page, 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_page!(model, Profile; Empty);
PageContent::Profile(profile_page) => profile_page,
_ => return empty![],
};
let avatar = StyledImageInput { let avatar = StyledImageInput {
id: FieldId::Profile(UsersFieldId::Avatar), id: FieldId::Profile(UsersFieldId::Avatar),
@ -87,12 +85,13 @@ pub fn view(model: &Model) -> Node<Msg> {
avatar, avatar,
username_field, username_field,
email_field, email_field,
editor_mode_select(page),
current_project, current_project,
submit_field, submit_field,
], ],
} }
.render(); .render();
inner_layout(model, "profile", &[content]) inner_layout(model, "profile", &[div![C!["formContainer"], content]])
} }
fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> { fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
@ -147,6 +146,7 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
.render() .render()
} }
#[inline(always)]
fn project_select_option<'l>(project: &'l Project) -> StyledSelectOption<'l> { fn project_select_option<'l>(project: &'l Project) -> StyledSelectOption<'l> {
StyledSelectOption { StyledSelectOption {
text: Some(project.name.as_str()), text: Some(project.name.as_str()),
@ -154,3 +154,47 @@ fn project_select_option<'l>(project: &'l Project) -> StyledSelectOption<'l> {
..Default::default() ..Default::default()
} }
} }
#[inline(always)]
fn editor_mode_select(page: &ProfilePage) -> Node<Msg> {
let time_tracking = StyledCheckbox {
options: Some(
vec![
TextEditorMode::MdOnly,
TextEditorMode::RteOnly,
TextEditorMode::Mixed,
]
.into_iter()
.map(|tem| editor_mode_checkbox_option(tem, &page.text_editor_mode)),
),
class_list: "timeTracking",
}
.render();
StyledField {
input: time_tracking,
label: "Text editor type",
tip: Some("You can choose if what kind of text editor you will have"),
..Default::default()
}
.render()
}
#[inline(always)]
fn editor_mode_checkbox_option<'l>(
tem: TextEditorMode,
state: &StyledCheckboxState,
) -> ChildBuilder<'l> {
let value: u32 = tem.into();
ChildBuilder {
field_id: state.field_id.clone(),
selected: state.value == value,
label: match tem {
TextEditorMode::MdOnly => "Simple Markdown editor",
TextEditorMode::RteOnly => "Advanced Rich Text Editor",
TextEditorMode::Mixed => "Editor with possibility to switch between modes",
},
class_list: tem.to_str(),
value,
..Default::default()
}
}

View File

@ -14,7 +14,7 @@ pub struct StatusIssueIds {
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct EpicIssuePerStatus { pub struct EpicIssuePerStatus {
pub epic_ref: Option<(EpicId, EpicName)>, pub epic_ref: Option<(EpicId, EpicName, Option<StartsAt>, Option<EndsAt>)>,
pub per_status_issues: Vec<StatusIssueIds>, pub per_status_issues: Vec<StatusIssueIds>,
} }
@ -36,9 +36,11 @@ impl ProjectPage {
issues: &[Issue], issues: &[Issue],
user: &Option<User>, user: &Option<User>,
) -> Vec<EpicIssuePerStatus> { ) -> Vec<EpicIssuePerStatus> {
let epics = vec![None] let epics = vec![None].into_iter().chain(
.into_iter() epics
.chain(epics.iter().map(|s| Some((s.id, s.name.as_str())))); .iter()
.map(|epic| Some((epic.id, epic.name.as_str(), epic.starts_at, epic.ends_at))),
);
let statuses = statuses.iter().map(|s| (s.id, s.name.as_str())); let statuses = statuses.iter().map(|s| (s.id, s.name.as_str()));
let issues = issues.iter().filter(|issue| { let issues = issues.iter().filter(|issue| {
@ -74,7 +76,9 @@ impl ProjectPage {
epics epics
.map(|epic| { .map(|epic| {
let mut per_epic_map = EpicIssuePerStatus { let mut per_epic_map = EpicIssuePerStatus {
epic_ref: epic.map(|(id, name)| (id, name.to_string())), epic_ref: epic.map(|(id, name, starts_at, ends_at)| {
(id, name.to_string(), starts_at, ends_at)
}),
..Default::default() ..Default::default()
}; };
@ -83,7 +87,7 @@ impl ProjectPage {
status_id: current_status_id, status_id: current_status_id,
status_name: issue_status_name.to_string(), status_name: issue_status_name.to_string(),
issue_ids: issues_per_epic_id issue_ids: issues_per_epic_id
.get(&epic.map(|(id, _)| id)) .get(&epic.map(|(id, ..)| id))
.map(|v| { .map(|v| {
v.iter() v.iter()
.filter(|issue| issue_filter_status(issue, current_status_id)) .filter(|issue| issue_filter_status(issue, current_status_id))

View File

@ -63,7 +63,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
Msg::ProjectAvatarFilterChanged(user_id, active) => { Msg::ProjectAvatarFilterChanged(user_id, active) => {
if active { if active {
project_page.active_avatar_filters = project_page.active_avatar_filters =
std::mem::replace(&mut project_page.active_avatar_filters, vec![]) std::mem::take(&mut project_page.active_avatar_filters)
.into_iter() .into_iter()
.filter(|id| *id != user_id) .filter(|id| *id != user_id)
.collect(); .collect();

View File

@ -12,6 +12,7 @@ use crate::{match_page, BoardPageChange, Model, Msg, Page, PageChanged};
pub fn project_board_lists(model: &Model) -> Node<Msg> { pub fn project_board_lists(model: &Model) -> Node<Msg> {
let project_page = match_page!(model, Project; Empty); let project_page = match_page!(model, Project; Empty);
let now = chrono::Utc::now().naive_utc();
let rows = project_page.visible_issues.iter().map(|per_epic| { let rows = project_page.visible_issues.iter().map(|per_epic| {
let columns: Vec<Node<Msg>> = per_epic let columns: Vec<Node<Msg>> = per_epic
.per_status_issues .per_status_issues
@ -31,7 +32,7 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
}) })
.collect(); .collect();
let epic_name = match per_epic.epic_ref.as_ref() { let epic_name = match per_epic.epic_ref.as_ref() {
Some((id, name)) => { Some((id, name, starts_at, ends_at)) => {
let id = *id; let id = *id;
let edit_button = StyledButton { let edit_button = StyledButton {
variant: ButtonVariant::Empty, variant: ButtonVariant::Empty,
@ -64,9 +65,25 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
} }
.render(); .render();
let range = match (starts_at, ends_at) {
(Some(s), Some(e)) => div![
C!["timeRange"],
div![C!["startsAt"], format!("{}", s.format("%e %B %Y"))],
div![C!["separator"], "-"],
div![
IF![now.date() > e.date() => C!["error"]],
IF![now.date() == e.date() => C!["warning"]],
C!["endsAt"],
format!("{}", e.format("%e %B %Y"))
]
],
_ => Node::Empty,
};
div![ div![
C!["epicHeader"], C!["epicHeader"],
div![C!["epicName"], name], div![C!["epicName"], name],
range,
div![C!["epicActions"], edit_button, delete_button], div![C!["epicActions"], edit_button, delete_button],
] ]
} }

View File

@ -1,6 +1,7 @@
use jirs_data::{IssueStatusId, Project, ProjectFieldId, UpdateProjectPayload}; use jirs_data::{IssueStatusId, Project, ProjectFieldId, TextEditorMode, UpdateProjectPayload};
use crate::components::styled_checkbox::StyledCheckboxState; use crate::components::styled_checkbox::StyledCheckboxState;
use crate::components::styled_editor::StyledEditorState;
use crate::components::styled_input::StyledInputState; use crate::components::styled_input::StyledInputState;
use crate::components::styled_select::StyledSelectState; use crate::components::styled_select::StyledSelectState;
use crate::shared::drag::DragState; use crate::shared::drag::DragState;
@ -10,17 +11,16 @@ use crate::FieldId;
pub struct ProjectSettingsPage { pub struct ProjectSettingsPage {
pub payload: UpdateProjectPayload, pub payload: UpdateProjectPayload,
pub project_category_state: StyledSelectState, pub project_category_state: StyledSelectState,
pub description_mode: crate::components::styled_editor::Mode,
pub time_tracking: StyledCheckboxState, pub time_tracking: StyledCheckboxState,
pub column_drag: DragState, pub column_drag: DragState,
pub edit_column_id: Option<IssueStatusId>, pub edit_column_id: Option<IssueStatusId>,
pub creating_issue_status: bool, pub creating_issue_status: bool,
pub name: StyledInputState, pub name: StyledInputState,
pub description: StyledEditorState,
} }
impl ProjectSettingsPage { impl ProjectSettingsPage {
pub fn new(project: &Project) -> Self { pub fn new(mode: TextEditorMode, project: &Project) -> Self {
use crate::components::styled_editor::Mode as EditorMode;
let jirs_data::Project { let jirs_data::Project {
id, id,
name, name,
@ -39,7 +39,6 @@ impl ProjectSettingsPage {
category: Some(*category), category: Some(*category),
time_tracking: Some(*time_tracking), time_tracking: Some(*time_tracking),
}, },
description_mode: EditorMode::View,
project_category_state: StyledSelectState::new( project_category_state: StyledSelectState::new(
FieldId::ProjectSettings(ProjectFieldId::Category), FieldId::ProjectSettings(ProjectFieldId::Category),
vec![(*category).into()], vec![(*category).into()],
@ -55,6 +54,12 @@ impl ProjectSettingsPage {
FieldId::ProjectSettings(ProjectFieldId::IssueStatusName), FieldId::ProjectSettings(ProjectFieldId::IssueStatusName),
"", "",
), ),
description: StyledEditorState::new(
FieldId::ProjectSettings(ProjectFieldId::DescriptionMode),
mode,
"",
"",
),
} }
} }

View File

@ -8,8 +8,7 @@ use crate::components::styled_select::StyledSelectChanged;
use crate::model::{Model, Page, PageContent}; use crate::model::{Model, Page, PageContent};
use crate::pages::project_settings_page::ProjectSettingsPage; use crate::pages::project_settings_page::ProjectSettingsPage;
use crate::ws::{board_load, send_ws_msg}; use crate::ws::{board_load, send_ws_msg};
use crate::FieldChange::TabChanged; use crate::{match_page_mut, FieldId, Msg, PageChanged, ProjectPageChange, WebSocketChanged};
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 {
@ -19,6 +18,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
Msg::ProjectChanged(Some(_)) => { Msg::ProjectChanged(Some(_)) => {
build_page_content(model); build_page_content(model);
send_ws_msg(WsMsg::IssueStatusesLoad, model.ws.as_ref(), orders);
} }
Msg::WebSocketChange(ref change) => match change { Msg::WebSocketChange(ref change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => { WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
@ -43,18 +43,28 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
_ => (), _ => (),
} }
if model.user.is_none() || model.project.is_none() { if model.user.is_none() {
return; return;
} }
let page = match &mut model.page_content { if model.project.is_none() {
PageContent::ProjectSettings(page) => page, board_load(model, orders);
_ => return error!("bad content type"), return;
}; }
if matches!(model.page, Page::ProjectSettings)
&& !matches!(model.page_content, PageContent::ProjectSettings(..))
{
build_page_content(model);
send_ws_msg(WsMsg::IssueStatusesLoad, model.ws.as_ref(), orders);
}
let page = match_page_mut!(model, ProjectSettings);
page.project_category_state.update(&msg, orders); page.project_category_state.update(&msg, orders);
page.time_tracking.update(&msg); page.time_tracking.update(&msg);
page.name.update(&msg); page.name.update(&msg);
// page.description_rte.update(&msg, orders); page.description.update(&msg, orders);
match msg { match msg {
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => { Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => {
@ -73,12 +83,6 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let category = value.into(); let category = value.into();
page.payload.category = Some(category); page.payload.category = Some(category);
} }
Msg::ModalChanged(TabChanged(
FieldId::ProjectSettings(ProjectFieldId::Description),
mode,
)) => {
page.description_mode = mode;
}
Msg::PageChanged(PageChanged::ProjectSettings( Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::SubmitProjectSettingsForm, ProjectPageChange::SubmitProjectSettingsForm,
)) => { )) => {
@ -234,9 +238,13 @@ fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
fn build_page_content(model: &mut Model) { fn build_page_content(model: &mut Model) {
let project = match &model.project { if let Some(project) = &model.project {
Some(project) => project, let mode = model
_ => return, .user_settings
}; .as_ref()
model.page_content = PageContent::ProjectSettings(Box::new(ProjectSettingsPage::new(project))); .map(|us| us.text_editor_mode)
.unwrap_or_default();
model.page_content =
PageContent::ProjectSettings(Box::new(ProjectSettingsPage::new(mode, project)));
}
} }

View File

@ -6,7 +6,7 @@ use seed::*;
use crate::components::styled_button::{ButtonVariant, StyledButton}; use crate::components::styled_button::{ButtonVariant, StyledButton};
use crate::components::styled_checkbox::{ChildBuilder, StyledCheckbox, StyledCheckboxState}; use crate::components::styled_checkbox::{ChildBuilder, StyledCheckbox, StyledCheckboxState};
use crate::components::styled_editor::StyledEditor; use crate::components::styled_editor::render_styled_editor;
use crate::components::styled_field::StyledField; use crate::components::styled_field::StyledField;
use crate::components::styled_form::StyledForm; use crate::components::styled_form::StyledForm;
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
@ -19,8 +19,6 @@ use crate::pages::project_settings_page::ProjectSettingsPage;
use crate::shared::inner_layout; use crate::shared::inner_layout;
use crate::{FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange}; use crate::{FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange};
// use crate::shared::styled_rte::StyledRte;
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");
@ -33,17 +31,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let url_field = url_field(page); let url_field = url_field(page);
let description_field = description_field(page); let description_field = render_styled_editor(&page.description);
// let desc_rte = StyledField::build()
// .input(
// StyledRte::build(FieldId::ProjectSettings(ProjectFieldId::
// Description)) .state(&page.description_rte)
// .build()
// .render(),
// )
// .build()
// .render();
let category_field = category_field(page); let category_field = category_field(page);
@ -114,6 +102,7 @@ fn time_tracking_select(page: &ProjectSettingsPage) -> Node<Msg> {
.render() .render()
} }
#[inline(always)]
fn time_tracking_checkbox_option<'l>( fn time_tracking_checkbox_option<'l>(
t: TimeTracking, t: TimeTracking,
state: &StyledCheckboxState, state: &StyledCheckboxState,
@ -138,10 +127,12 @@ fn time_tracking_checkbox_option<'l>(
TimeTracking::Hourly => "hourly", TimeTracking::Hourly => "hourly",
}, },
value, value,
..Default::default()
} }
} }
/// Build project name input with styled field wrapper /// Build project name input with styled field wrapper
#[inline(always)]
fn name_field(page: &ProjectSettingsPage) -> Node<Msg> { fn name_field(page: &ProjectSettingsPage) -> Node<Msg> {
let name = StyledTextarea { let name = StyledTextarea {
id: Some(FieldId::ProjectSettings(ProjectFieldId::Name)), id: Some(FieldId::ProjectSettings(ProjectFieldId::Name)),
@ -162,6 +153,7 @@ fn name_field(page: &ProjectSettingsPage) -> Node<Msg> {
} }
/// Build project url input with styled field wrapper /// Build project url input with styled field wrapper
#[inline(always)]
fn url_field(page: &ProjectSettingsPage) -> Node<Msg> { fn url_field(page: &ProjectSettingsPage) -> Node<Msg> {
let url = StyledTextarea { let url = StyledTextarea {
id: Some(FieldId::ProjectSettings(ProjectFieldId::Url)), id: Some(FieldId::ProjectSettings(ProjectFieldId::Url)),
@ -181,27 +173,8 @@ fn url_field(page: &ProjectSettingsPage) -> Node<Msg> {
.render() .render()
} }
/// Build project description text area with styled field wrapper
fn description_field(page: &ProjectSettingsPage) -> Node<Msg> {
let description = StyledEditor {
id: Some(FieldId::ProjectSettings(ProjectFieldId::Description)),
initial_text: page.payload.description.as_deref().unwrap_or_default(),
text: page.payload.description.as_deref().unwrap_or_default(),
html: page.payload.description.as_deref().unwrap_or_default(),
mode: page.description_mode.clone(),
update_event: Ev::Change,
}
.render();
StyledField {
label: "Description",
tip: Some("Describe the project in as much detail as you'd like."),
input: description,
..Default::default()
}
.render()
}
/// Build project category dropdown with styled field wrapper /// Build project category dropdown with styled field wrapper
#[inline(always)]
fn category_field(page: &ProjectSettingsPage) -> Node<Msg> { fn category_field(page: &ProjectSettingsPage) -> Node<Msg> {
let category = StyledSelect { let category = StyledSelect {
id: FieldId::ProjectSettings(ProjectFieldId::Category), id: FieldId::ProjectSettings(ProjectFieldId::Category),
@ -239,6 +212,7 @@ fn category_select_option<'l>(pc: ProjectCategory) -> StyledSelectOption<'l> {
} }
/// Build draggable columns preview with option to remove and add new columns /// Build draggable columns preview with option to remove and add new columns
#[inline(always)]
fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> { fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
let width = 100f64 / (model.issue_statuses.len() + 1) as f64; let width = 100f64 / (model.issue_statuses.len() + 1) as f64;
let column_style = format!("width: calc({width}% - 10px)", width = width); let column_style = format!("width: calc({width}% - 10px)", width = width);
@ -269,7 +243,7 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
.render() .render()
} }
#[inline] #[inline(always)]
fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> { fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
let on_click = mouse_ev(Ev::Click, move |_| { let on_click = mouse_ev(Ev::Click, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings( Msg::PageChanged(PageChanged::ProjectSettings(
@ -317,7 +291,7 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
} }
} }
#[inline] #[inline(always)]
fn column_preview( fn column_preview(
is: &IssueStatus, is: &IssueStatus,
page: &ProjectSettingsPage, page: &ProjectSettingsPage,
@ -347,6 +321,7 @@ fn column_preview(
} }
} }
#[inline(always)]
fn show_column_preview( fn show_column_preview(
is: &IssueStatus, is: &IssueStatus,
per_column_issue_count: &HashMap<i32, i32>, per_column_issue_count: &HashMap<i32, i32>,

View File

@ -1,3 +1,14 @@
use crate::shared::validate::*;
use crate::validations::*;
use crate::validator;
validator!(EmailFormat, is_email, "Not a valid e-mail address");
validator!(UuidFormat, is_token, "Malformed token");
pub type UsernameValidator = Touched<Between<4, 20>>;
pub type EmailValidator = Touched<Chain<Changed<AtLeast<6>>, Changed<EmailFormat>>>;
pub type TokenValidator = Touched<Chain<Between<10, 20>, Changed<UuidFormat>>>;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct SignInPage { pub struct SignInPage {
pub username: String, pub username: String,
@ -5,8 +16,8 @@ pub struct SignInPage {
pub token: String, pub token: String,
pub login_success: bool, pub login_success: bool,
pub bad_token: String, pub bad_token: String,
// touched // validators
pub username_touched: bool, pub username_v: UsernameValidator,
pub email_touched: bool, pub email_v: EmailValidator,
pub token_touched: bool, pub token_v: TokenValidator,
} }

View File

@ -7,37 +7,35 @@ use uuid::Uuid;
use crate::model::{self, Model, Page, PageContent}; use crate::model::{self, Model, Page, PageContent};
use crate::pages::sign_in_page::model::SignInPage; use crate::pages::sign_in_page::model::SignInPage;
use crate::shared::validate::*;
use crate::shared::write_auth_token; use crate::shared::write_auth_token;
use crate::ws::send_ws_msg; use crate::ws::send_ws_msg;
use crate::{FieldId, Msg, WebSocketChanged}; use crate::{match_page_mut, FieldId, Msg, WebSocketChanged};
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::SignIn { if model.page != Page::SignIn {
return; return;
} } else if !matches!(model.page_content, PageContent::SignIn(..)) {
build_page_content(model);
if let Msg::ChangePage(Page::SignIn) = msg { } else if matches!(msg, Msg::ChangePage(Page::SignIn)) {
build_page_content(model); build_page_content(model);
return; return;
}; };
let page = match &mut model.page_content { let page = match_page_mut!(model, SignIn);
PageContent::SignIn(page) => page,
_ => return,
};
match msg { match msg {
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Username), value) => { Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Username), value) => {
page.username_v.validate(&value);
page.username = value; page.username = value;
page.username_touched = true;
} }
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Email), value) => { Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Email), value) => {
page.email_v.validate(&value);
page.email = value; page.email = value;
page.email_touched = true;
} }
Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Token), value) => { Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Token), value) => {
page.token_v.validate(&value);
page.token = value; page.token = value;
page.token_touched = true;
} }
Msg::SignInRequest => { Msg::SignInRequest => {
send_ws_msg( send_ws_msg(

View File

@ -7,21 +7,18 @@ use crate::components::styled_form::StyledForm;
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
use crate::components::styled_input::StyledInput; use crate::components::styled_input::StyledInput;
use crate::components::styled_link::StyledLink; use crate::components::styled_link::StyledLink;
use crate::model::{self, PageContent};
use crate::shared::outer_layout; use crate::shared::outer_layout;
use crate::validations::{is_email, is_token}; use crate::shared::validate::Validator;
use crate::{FieldId, Msg, SignInFieldId}; use crate::{match_page, model, FieldId, Msg, SignInFieldId};
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_page!(model, SignIn; Empty);
PageContent::SignIn(page) => page,
_ => return empty![],
};
let username = StyledInput { let username = StyledInput {
value: page.username.as_str(), value: page.username.as_str(),
valid: is_valid_username(page.username_touched, &page.username), valid: page.username_v.is_valid(),
id: Some(FieldId::SignIn(SignInFieldId::Username)), id: Some(FieldId::SignIn(SignInFieldId::Username)),
err_msg: page.username_v.message(),
..Default::default() ..Default::default()
} }
.render(); .render();
@ -34,8 +31,9 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let email = StyledInput { let email = StyledInput {
value: page.email.as_str(), value: page.email.as_str(),
valid: is_valid_email(page.email_touched, page.email.as_str()), valid: page.email_v.is_valid(),
id: Some(FieldId::SignIn(SignInFieldId::Email)), id: Some(FieldId::SignIn(SignInFieldId::Email)),
err_msg: page.email_v.message(),
..Default::default() ..Default::default()
} }
.render(); .render();
@ -99,11 +97,13 @@ pub fn view(model: &model::Model) -> Node<Msg> {
} }
.render(); .render();
let token = StyledInput::new_with_id_and_value_and_valid( let token = StyledInput {
FieldId::SignIn(SignInFieldId::Token), id: Some(FieldId::SignIn(SignInFieldId::Token)),
&page.token, valid: page.token_v.is_valid(),
is_valid_token(page.token_touched, page.token.as_str()), value: page.token.as_str(),
) err_msg: page.token_v.message(),
..Default::default()
}
.render(); .render();
let token_field = StyledField { let token_field = StyledField {
label: "Single use token", label: "Single use token",
@ -138,15 +138,3 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let children = vec![sign_in_form, bind_token_form]; let children = vec![sign_in_form, bind_token_form];
outer_layout(model, "login", children) outer_layout(model, "login", children)
} }
fn is_valid_username(touched: bool, s: &str) -> bool {
!touched || (s.len() > 1 && s.len() < 20)
}
fn is_valid_email(touched: bool, s: &str) -> bool {
!touched || (is_email(s) && s.len() < 20)
}
fn is_valid_token(touched: bool, s: &str) -> bool {
!touched || is_token(s)
}

View File

@ -8,10 +8,9 @@ use crate::components::styled_form::StyledForm;
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
use crate::components::styled_input::StyledInput; use crate::components::styled_input::StyledInput;
use crate::components::styled_link::StyledLink; use crate::components::styled_link::StyledLink;
use crate::model::{self, PageContent};
use crate::shared::outer_layout; use crate::shared::outer_layout;
use crate::validations::is_email; use crate::validations::is_email;
use crate::{match_page, FieldId, Msg}; use crate::{match_page, model, FieldId, Msg};
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let page = match_page!(model, SignUp; Empty); let page = match_page!(model, SignUp; Empty);

View File

@ -1,15 +1,13 @@
use jirs_data::{InvitationState, UserRole, UsersFieldId, WsMsg}; use jirs_data::{InvitationState, UserRole, UsersFieldId, WsMsg};
use seed::log;
use seed::prelude::Orders; use seed::prelude::Orders;
use crate::components::styled_select::StyledSelectChanged; use crate::components::styled_select::StyledSelectChanged;
use crate::model::{InvitationFormState, Model, Page, PageContent}; use crate::model::{InvitationFormState, Model, Page, PageContent};
use crate::pages::users_page::model::UsersPage; use crate::pages::users_page::model::UsersPage;
use crate::ws::{invitation_load, send_ws_msg}; use crate::ws::{invitation_load, send_ws_msg};
use crate::{FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged}; use crate::{match_page_mut, 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>) {
log!(model);
if model.user.is_none() { if model.user.is_none() {
return; return;
} }
@ -18,10 +16,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
build_page_content(model); build_page_content(model);
} }
let page = match &mut model.page_content { let page = match_page_mut!(model, Users);
PageContent::Users(page) => page,
_ => return,
};
page.user_role_state.update(&msg, orders); page.user_role_state.update(&msg, orders);

View File

@ -1,11 +0,0 @@
fn main() {
// std::fs::create_dir_all("./jirs-client/src/hi/syntax").unwrap();
//
// use syntect::{dumps::*, parsing::*};
// let syntax_set: SyntaxSet = from_binary(include_bytes!("./hi/newlines.packdump"));
// std::fs::write(
// "./jirs-client/src/hi/syntax_set.cbor",
// serde_cbor::ser::to_vec(&syntax_set).unwrap(),
// )
// .unwrap();
}

View File

@ -7,7 +7,9 @@ use crate::{resolve_page, Msg};
pub mod aside; pub mod aside;
pub mod drag; pub mod drag;
pub mod navbar_left; pub mod navbar_left;
pub mod on_event;
pub mod tracking_widget; pub mod tracking_widget;
pub mod validate;
pub trait ToChild<'l> { pub trait ToChild<'l> {
type Builder: 'l; type Builder: 'l;

View File

@ -0,0 +1,153 @@
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::atomic::AtomicUsize;
use futures::StreamExt;
use seed::prelude::*;
use seed::*;
use wasm_bindgen_futures::spawn_local;
#[derive(Debug, Default)]
pub struct Distinct {
state: Rc<RefCell<HashSet<&'static str>>>,
locks: Rc<RefCell<HashMap<&'static str, bool>>>,
}
impl Distinct {
pub fn new() -> Self {
Self {
state: Rc::new(RefCell::new(Default::default())),
locks: Rc::new(RefCell::new(Default::default())),
}
}
pub fn keyup_wih_reset<Cb>(&self, selector: &'static str, submit_after_frames: usize, cb: Cb)
where
Cb: Fn(web_sys::KeyboardEvent) + 'static,
{
if self.state.borrow().contains(selector) {
return;
}
self.state.borrow_mut().insert(selector);
let callback = Rc::new(cb);
let locks = self.locks.clone();
on("keyup", move |ev| {
let active = match seed::document().active_element() {
None => {
return;
}
Some(el) => el,
};
if !active.class_name().contains(selector) {
return;
}
let locks = locks.clone();
if locks.borrow().get(selector).cloned().unwrap_or_default() {
return;
}
locks.borrow_mut().insert(selector, true);
let callback = callback.clone();
keyup_wih_reset(
selector,
move || {
let locks = locks.clone();
locks.borrow_mut().remove(selector);
let callback = callback.clone();
callback(ev);
},
submit_after_frames,
);
});
}
}
impl Clone for Distinct {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
locks: self.locks.clone(),
}
}
}
pub fn distinct() -> Distinct {
Distinct::new()
}
pub fn keydown<Cb>(cb: Cb)
where
Cb: FnMut(web_sys::KeyboardEvent) + 'static,
{
on("keydown", cb);
}
fn keyup_wih_reset<Cb>(selector: &'static str, cb: Cb, submit_after_frames: usize)
where
Cb: FnOnce() + 'static,
{
use std::sync::atomic::Ordering;
let (up_sender, mut up_receiver) = ::futures::channel::mpsc::unbounded();
let n: Rc<AtomicUsize> = Rc::new(AtomicUsize::new(submit_after_frames));
let reset = n.clone();
spawn_local(async move {
while up_receiver.next().await.is_some() {
reset.store(submit_after_frames, Ordering::Relaxed);
}
});
spawn_local(async move {
loop {
if n.load(Ordering::Relaxed) == 0 {
break;
}
wait_frame().await;
let _ = n.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| Some(v - 1));
}
cb();
});
on("keyup", move |_ev| {
let active = match seed::document().active_element() {
None => return,
Some(el) => el,
};
if !active.class_name().contains(selector) {
return;
}
let _ = up_sender.unbounded_send(());
});
}
pub fn keypress<Cb>(cb: Cb)
where
Cb: FnMut(web_sys::KeyboardEvent) + 'static,
{
on("keypress", cb);
}
pub fn on<Cb>(event: &str, cb: Cb)
where
Cb: FnMut(web_sys::KeyboardEvent) + 'static,
{
let handler = Closure::wrap(Box::new(cb) as Box<dyn FnMut(_)>);
seed::window()
.add_event_listener_with_callback(event, handler.as_ref().unchecked_ref())
.expect("Failed to mount global key handler");
handler.forget();
}
pub async fn wait_frame() -> f64 {
let (sender, mut receiver) = ::futures::channel::mpsc::unbounded();
let handler = Closure::wrap(Box::new(move |f| {
let _ = sender.unbounded_send(f);
}) as Box<dyn FnMut(f64)>);
let _ = seed::window().request_animation_frame(handler.as_ref().unchecked_ref());
handler.forget();
receiver.next().await.unwrap_or_default()
}

View File

@ -0,0 +1,207 @@
#[macro_export]
macro_rules! validator {
($id: ident, $is_valid: expr, $msg: expr) => {
#[derive(Debug, Default)]
pub struct $id;
impl Validator for $id {
fn validate(&mut self, value: &str) -> bool {
$is_valid(value)
}
fn message(&self) -> &'static str {
if self.is_valid() {
""
} else {
$msg
}
}
}
};
($id: ident, $const_name: ident, $const_ty: ty, $const_cond: ident, $msg: expr) => {
#[derive(Debug, Default)]
pub struct $id<const $const_name: $const_ty>;
impl<const $const_name: $const_ty> Validator for $id<$const_name> {
fn validate(&mut self, value: &str) -> bool {
$const_cond::<$const_name>(value)
}
fn message(&self) -> &'static str {
if self.is_valid() {
""
} else {
$msg
}
}
}
};
}
pub trait Validator: std::fmt::Debug {
fn validate(&mut self, value: &str) -> bool;
fn is_valid(&self) -> bool {
false
}
fn message(&self) -> &'static str {
""
}
}
#[derive(Debug, Default)]
pub struct Chain<A, B>(A, B)
where
A: Validator,
B: Validator;
impl<A, B> Validator for Chain<A, B>
where
A: Validator,
B: Validator,
{
fn validate(&mut self, value: &str) -> bool {
// trigger both
let left = self.0.validate(value);
let right = self.1.validate(value);
left && right
}
fn is_valid(&self) -> bool {
self.0.is_valid() && self.1.is_valid()
}
fn message(&self) -> &'static str {
match (!self.0.is_valid(), !self.1.is_valid()) {
(true, _) => self.0.message(),
(_, true) => self.1.message(),
_ => "",
}
}
}
#[derive(Debug, Default)]
pub struct AlwaysValid;
impl Validator for AlwaysValid {
fn validate(&mut self, _: &str) -> bool {
true
}
fn is_valid(&self) -> bool {
true
}
}
fn is_more_eq<const MIN: usize>(value: &str) -> bool {
value.len() >= MIN
}
fn is_less_eq<const MAX: usize>(value: &str) -> bool {
value.len() <= MAX
}
validator!(AtLeast, MIN, usize, is_more_eq, "Value is too short");
validator!(AtMost, MAX, usize, is_less_eq, "Value is too long");
pub type Between<const MIN: usize, const MAX: usize> =
Chain<Changed<AtLeast<MIN>>, Changed<AtMost<MAX>>>;
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub enum TouchState {
Untouched,
Touched,
}
impl Default for TouchState {
fn default() -> Self {
TouchState::Untouched
}
}
#[derive(Debug, Default)]
pub struct Touched<Valid>(pub Valid, TouchState)
where
Valid: Validator;
impl<V> Validator for Touched<V>
where
V: Validator,
{
fn validate(&mut self, value: &str) -> bool {
self.1 = TouchState::Touched;
self.0.validate(value)
}
fn is_valid(&self) -> bool {
match self.1 {
TouchState::Untouched => true,
_ => self.0.is_valid(),
}
}
fn message(&self) -> &'static str {
self.0.message()
}
}
#[derive(Debug)]
pub enum ChangedState {
Value(bool),
Unset,
}
impl Default for ChangedState {
fn default() -> Self {
ChangedState::Unset
}
}
#[derive(Debug, Default)]
pub struct Changed<V>(V, ChangedState)
where
V: Validator;
impl<V> Validator for Changed<V>
where
V: Validator,
{
fn validate(&mut self, value: &str) -> bool {
let res = self.0.validate(value);
self.1 = ChangedState::Value(res);
res
}
fn is_valid(&self) -> bool {
match self.1 {
ChangedState::Value(b) => b,
_ => self.0.is_valid(),
}
}
fn message(&self) -> &'static str {
self.0.message()
}
}
pub trait Validate<V: Validator> {
fn validator_mut(&mut self) -> Option<&mut V>;
fn validator(&self) -> Option<&V>;
fn is_valid(&self) -> bool {
self.validator().map(|v| v.is_valid()).unwrap_or(true)
}
fn validate(&mut self, value: &str) -> bool {
self.validator_mut()
.map(|v| v.validate(value))
.unwrap_or(true)
}
fn err_msg(&self) -> &'static str {
self.validator().map(|v| v.message()).unwrap_or("")
}
}

View File

@ -50,7 +50,7 @@ pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
use seed::browser::web_socket::State; use seed::browser::web_socket::State;
use seed::prelude::*; use seed::prelude::*;
use seed::*; use seed::*;
log!(model.ws.as_ref().map(|ws| ws.state())); log::warn!("{:?}", model.ws.as_ref().map(|ws| ws.state()));
match model.ws.as_ref() { match model.ws.as_ref() {
Some(ws) if ws.state() != State::Closed => { Some(ws) if ws.state() != State::Closed => {
@ -70,7 +70,7 @@ pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
))) )))
}) })
.on_open(|| { .on_open(|| {
log!("open_socket opened"); log::info!("open_socket opened");
Some(Msg::WebSocketChange(WebSocketChanged::WebSocketOpened)) Some(Msg::WebSocketChange(WebSocketChanged::WebSocketOpened))
}) })
.on_close(|_| Some(Msg::WebSocketChange(WebSocketChanged::WebSocketClosed))) .on_close(|_| Some(Msg::WebSocketChange(WebSocketChanged::WebSocketClosed)))
@ -91,11 +91,14 @@ pub async fn read_incoming(msg: WebSocketMessage) -> Msg {
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, setting))) => {
model.user = Some(user); model.user = Some(user);
model.user_settings = Some(setting);
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()))
@ -104,6 +107,11 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
OperationKind::SingleLoaded, OperationKind::SingleLoaded,
model.user.as_ref().map(|u| u.id), model.user.as_ref().map(|u| u.id),
)) ))
.send_msg(Msg::ResourceChanged(
ResourceKind::UserSetting,
OperationKind::SingleLoaded,
model.user.as_ref().map(|u| u.id),
))
.send_msg(Msg::ResourceChanged( .send_msg(Msg::ResourceChanged(
ResourceKind::Auth, ResourceKind::Auth,
OperationKind::SingleLoaded, OperationKind::SingleLoaded,
@ -111,9 +119,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
)); ));
} }
WsMsg::AuthorizeExpired => { WsMsg::AuthorizeExpired => {
use seed::*; log::warn!("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).send_msg(Msg::ResourceChanged( orders.skip().send_msg(msg).send_msg(Msg::ResourceChanged(
ResourceKind::Auth, ResourceKind::Auth,
@ -158,6 +164,15 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
model.current_user_project.as_ref().map(|up| up.id), model.current_user_project.as_ref().map(|up| up.id),
)); ));
} }
// user settings
WsMsg::UserSettingUpdated(setting) => {
model.user_settings = Some(setting);
orders.send_msg(Msg::ResourceChanged(
ResourceKind::UserSetting,
OperationKind::SingleModified,
model.user_settings.as_ref().map(|up| up.id),
));
}
// issue statuses // issue statuses
WsMsg::IssueStatusesLoaded(v) => { WsMsg::IssueStatusesLoaded(v) => {

View File

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS user_settings;
DROP TYPE "TextEditorModeType";

View File

@ -0,0 +1,12 @@
CREATE TYPE "TextEditorModeType" AS ENUM (
'md_only',
'rte_only',
'mixed'
);
CREATE TABLE IF NOT EXISTS user_settings
(
id serial not null unique primary key,
user_id int references users (id) not null,
text_editor_mode "TextEditorModeType" DEFAULT 'md_only' NOT NULL
);

View File

@ -5,6 +5,7 @@ pub enum ProjectFieldId {
Name, Name,
Url, Url,
Description, Description,
DescriptionMode,
Category, Category,
TimeTracking, TimeTracking,
IssueStatusName, IssueStatusName,
@ -30,6 +31,7 @@ pub enum UsersFieldId {
UserRole, UserRole,
Avatar, Avatar,
CurrentProject, CurrentProject,
TextEditorMode,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]

View File

@ -24,6 +24,7 @@ pub type ListPosition = i32;
pub type ProjectId = i32; pub type ProjectId = i32;
pub type ProjectName = String; pub type ProjectName = String;
pub type UserId = i32; pub type UserId = i32;
pub type UserSettingId = i32;
pub type UserProjectId = i32; pub type UserProjectId = i32;
pub type CommentId = i32; pub type CommentId = i32;
pub type TokenId = i32; pub type TokenId = i32;
@ -48,6 +49,9 @@ pub type Lang = String;
pub type BindToken = Uuid; pub type BindToken = Uuid;
pub type InvitationToken = Uuid; pub type InvitationToken = Uuid;
pub type StartsAt = NaiveDateTime;
pub type EndsAt = NaiveDateTime;
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))] #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
#[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")]
#[derive( #[derive(
@ -384,8 +388,8 @@ pub struct Epic {
pub project_id: ProjectId, pub project_id: ProjectId,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub starts_at: Option<NaiveDateTime>, pub starts_at: Option<StartsAt>,
pub ends_at: Option<NaiveDateTime>, pub ends_at: Option<EndsAt>,
pub description: Option<DescriptionString>, pub description: Option<DescriptionString>,
pub description_html: Option<DescriptionString>, pub description_html: Option<DescriptionString>,
} }
@ -422,3 +426,29 @@ pub struct Style {
pub struct HighlightedCode { pub struct HighlightedCode {
pub parts: Vec<(Style, String)>, pub parts: Vec<(Style, String)>,
} }
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
#[cfg_attr(feature = "backend", sql_type = "TextEditorModeType")]
#[derive(
Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive,
)]
#[repr(C)]
pub enum TextEditorMode {
MdOnly,
RteOnly,
Mixed,
}
impl Default for TextEditorMode {
fn default() -> Self {
TextEditorMode::MdOnly
}
}
#[cfg_attr(feature = "backend", derive(Queryable))]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct UserSetting {
pub id: UserSettingId,
pub user_id: UserId,
pub text_editor_mode: TextEditorMode,
}

View File

@ -3,11 +3,12 @@ use uuid::Uuid;
use crate::{ use crate::{
AvatarUrl, BindToken, Code, Comment, CommentId, CreateCommentPayload, CreateIssuePayload, AvatarUrl, BindToken, Code, Comment, CommentId, CreateCommentPayload, CreateIssuePayload,
DescriptionString, EmailString, Epic, EpicId, HighlightedCode, Invitation, InvitationId, DescriptionString, EmailString, EndsAt, Epic, EpicId, HighlightedCode, Invitation,
InvitationToken, Issue, IssueFieldId, IssueId, IssueStatus, IssueStatusId, IssueType, Lang, InvitationId, InvitationToken, Issue, IssueFieldId, IssueId, IssueStatus, IssueStatusId,
ListPosition, Message, MessageId, NameString, NumberOfDeleted, PayloadVariant, Position, IssueType, Lang, ListPosition, Message, MessageId, NameString, NumberOfDeleted, PayloadVariant,
Project, TitleString, UpdateCommentPayload, UpdateProjectPayload, User, UserId, UserProject, Position, Project, StartsAt, TextEditorMode, TitleString, UpdateCommentPayload,
UserProjectId, UserRole, UsernameString, UpdateProjectPayload, User, UserId, UserProject, UserProjectId, UserRole, UserSetting,
UsernameString,
}; };
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
@ -126,7 +127,7 @@ pub enum WsMsg {
// auth // auth
AuthorizeLoad(Uuid), AuthorizeLoad(Uuid),
AuthorizeLoaded(Result<User, String>), AuthorizeLoaded(Result<(User, UserSetting), String>),
AuthorizeExpired, AuthorizeExpired,
AuthenticateRequest(EmailString, UsernameString), AuthenticateRequest(EmailString, UsernameString),
AuthenticateSuccess, AuthenticateSuccess,
@ -212,6 +213,10 @@ pub enum WsMsg {
ProfileUpdate(EmailString, UsernameString), ProfileUpdate(EmailString, UsernameString),
ProfileUpdated, ProfileUpdated,
// user settings
UserSettingUpdated(UserSetting),
UserSettingSetEditorMode(TextEditorMode),
// user projects // user projects
UserProjectsLoad, UserProjectsLoad,
UserProjectsLoaded(Vec<UserProject>), UserProjectsLoaded(Vec<UserProject>),
@ -234,7 +239,9 @@ pub enum WsMsg {
Option<DescriptionString>, Option<DescriptionString>,
), ),
EpicCreated(Epic), EpicCreated(Epic),
EpicUpdate(EpicId, NameString), EpicUpdateName(EpicId, NameString),
EpicUpdateStartsAt(EpicId, Option<StartsAt>),
EpicUpdateEndsAt(EpicId, Option<EndsAt>),
EpicUpdated(Epic), EpicUpdated(Epic),
EpicDelete(EpicId), EpicDelete(EpicId),
EpicDeleted(EpicId, NumberOfDeleted), EpicDeleted(EpicId, NumberOfDeleted),