Upgrade to latest Seed, remove JS WebSocket, remove webpack dependency

This commit is contained in:
Adrian Wozniak 2020-05-19 11:55:55 +02:00
parent 9625cde0f6
commit 39ee462e33
34 changed files with 1135 additions and 1024 deletions

358
Cargo.lock generated
View File

@ -15,7 +15,7 @@ dependencies = [
"derive_more",
"futures 0.3.4",
"lazy_static",
"log 0.4.8",
"log",
"parking_lot",
"pin-project",
"smallvec",
@ -35,7 +35,7 @@ dependencies = [
"bytes",
"futures-core",
"futures-sink",
"log 0.4.8",
"log",
"tokio",
"tokio-util",
]
@ -54,7 +54,7 @@ dependencies = [
"either",
"futures 0.3.4",
"http",
"log 0.4.8",
"log",
"trust-dns-proto",
"trust-dns-resolver",
]
@ -84,10 +84,10 @@ dependencies = [
"bytes",
"derive_more",
"futures 0.3.4",
"log 0.4.8",
"log",
"mime",
"mime_guess",
"percent-encoding 2.1.0",
"percent-encoding",
"v_htmlescape",
]
@ -124,12 +124,12 @@ dependencies = [
"indexmap",
"language-tags",
"lazy_static",
"log 0.4.8",
"log",
"mime",
"percent-encoding 2.1.0",
"percent-encoding",
"pin-project",
"rand 0.7.3",
"regex 1.3.6",
"regex",
"serde",
"serde_json",
"serde_urlencoded",
@ -161,7 +161,7 @@ dependencies = [
"derive_more",
"futures 0.3.4",
"httparse",
"log 0.4.8",
"log",
"mime",
"time 0.1.42",
"twoway",
@ -175,8 +175,8 @@ checksum = "9d7a10ca4d94e8c8e7a87c5173aba1b97ba9a6563ca02b0e1cd23531093d3ec8"
dependencies = [
"bytestring",
"http",
"log 0.4.8",
"regex 1.3.6",
"log",
"regex",
"serde",
]
@ -204,7 +204,7 @@ dependencies = [
"actix-service",
"actix-utils",
"futures 0.3.4",
"log 0.4.8",
"log",
"mio",
"mio-uds",
"net2",
@ -233,7 +233,7 @@ dependencies = [
"actix-server",
"actix-service",
"futures 0.3.4",
"log 0.4.8",
"log",
"net2",
]
@ -246,7 +246,7 @@ dependencies = [
"derive_more",
"futures-channel",
"lazy_static",
"log 0.4.8",
"log",
"num_cpus",
"parking_lot",
"threadpool",
@ -265,7 +265,7 @@ dependencies = [
"derive_more",
"either",
"futures 0.3.4",
"log 0.4.8",
"log",
]
[[package]]
@ -281,7 +281,7 @@ dependencies = [
"bytes",
"either",
"futures 0.3.4",
"log 0.4.8",
"log",
"pin-project",
"slab",
]
@ -310,16 +310,16 @@ dependencies = [
"encoding_rs",
"futures 0.3.4",
"fxhash",
"log 0.4.8",
"log",
"mime",
"net2",
"pin-project",
"regex 1.3.6",
"regex",
"serde",
"serde_json",
"serde_urlencoded",
"time 0.1.42",
"url 2.1.1",
"url",
]
[[package]]
@ -365,22 +365,13 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
[[package]]
name = "aho-corasick"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
dependencies = [
"memchr 0.1.11",
]
[[package]]
name = "aho-corasick"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
dependencies = [
"memchr 2.3.3",
"memchr",
]
[[package]]
@ -464,9 +455,9 @@ dependencies = [
"bytes",
"derive_more",
"futures-core",
"log 0.4.8",
"log",
"mime",
"percent-encoding 2.1.0",
"percent-encoding",
"rand 0.7.3",
"serde",
"serde_json",
@ -709,7 +700,7 @@ dependencies = [
"lazy_static",
"pest",
"pest_derive",
"regex 1.3.6",
"regex",
"twoway",
"typed-arena",
"unicode_categories",
@ -733,12 +724,12 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "cookie"
version = "0.12.0"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5"
checksum = "0c60ef6d0bbf56ad2674249b6bb74f2c6aeb98b98dd57b5d3e37cace33011d69"
dependencies = [
"time 0.1.42",
"url 1.7.2",
"percent-encoding",
"time 0.2.15",
]
[[package]]
@ -1025,16 +1016,6 @@ dependencies = [
"syn",
]
[[package]]
name = "env_logger"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f"
dependencies = [
"log 0.3.9",
"regex 0.1.80",
]
[[package]]
name = "env_logger"
version = "0.7.1"
@ -1043,8 +1024,8 @@ checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [
"atty",
"humantime",
"log 0.4.8",
"regex 1.3.6",
"log",
"regex",
"termcolor",
]
@ -1261,7 +1242,7 @@ dependencies = [
"futures-macro",
"futures-sink",
"futures-task",
"memchr 2.3.3",
"memchr",
"pin-utils",
"proc-macro-hack",
"proc-macro-nested",
@ -1304,6 +1285,7 @@ dependencies = [
"cfg-if",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@ -1312,12 +1294,37 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "gloo-events"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088514ec8ef284891c762c88a66b639b3a730134714692ee31829765c5bc814f"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-file"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f9fecfe46b5dc3cc46f58e98ba580cc714f2c93860796d002eb3527a465ef49"
dependencies = [
"futures-channel",
"gloo-events",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-timers"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
"web-sys",
@ -1336,7 +1343,7 @@ dependencies = [
"futures-util",
"http",
"indexmap",
"log 0.4.8",
"log",
"slab",
"tokio",
"tokio-util",
@ -1448,7 +1455,7 @@ dependencies = [
"http-body",
"httparse",
"itoa",
"log 0.4.8",
"log",
"net2",
"pin-project",
"time 0.1.42",
@ -1470,17 +1477,6 @@ dependencies = [
"tokio-tls",
]
[[package]]
name = "idna"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.2.0"
@ -1600,21 +1596,20 @@ dependencies = [
"chrono",
"diesel",
"dotenv",
"env_logger 0.7.1",
"env_logger",
"futures 0.3.4",
"ipnetwork",
"jirs-data",
"lettre",
"lettre_email",
"libc",
"log 0.4.8",
"log",
"num-bigint",
"num-integer",
"num-traits",
"percent-encoding 2.1.0",
"percent-encoding",
"pq-sys",
"pretty_env_logger",
"quickcheck",
"r2d2",
"rusoto_core",
"rusoto_s3",
@ -1622,7 +1617,7 @@ dependencies = [
"serde_json",
"time 0.1.42",
"toml",
"url 2.1.1",
"url",
"uuid 0.8.1",
]
@ -1647,9 +1642,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.37"
version = "0.3.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055"
checksum = "fa5a448de267e7358beaf4a5d849518fe9a0c13fce7afd44b06e68550e5562a7"
dependencies = [
"wasm-bindgen",
]
@ -1692,7 +1687,7 @@ dependencies = [
"bufstream",
"fast_chemail",
"hostname 0.1.5",
"log 0.4.8",
"log",
"native-tls",
"nom",
"serde",
@ -1735,15 +1730,6 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
dependencies = [
"log 0.4.8",
]
[[package]]
name = "log"
version = "0.4.8"
@ -1792,15 +1778,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20"
dependencies = [
"libc",
]
[[package]]
name = "memchr"
version = "2.3.3"
@ -1844,7 +1821,7 @@ dependencies = [
"iovec",
"kernel32-sys",
"libc",
"log 0.4.8",
"log",
"miow 0.2.1",
"net2",
"slab",
@ -1858,7 +1835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log 0.4.8",
"log",
"mio",
"slab",
]
@ -1869,7 +1846,7 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3"
dependencies = [
"log 0.4.8",
"log",
"mio",
"miow 0.3.3",
"winapi 0.3.8",
@ -1916,7 +1893,7 @@ checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
dependencies = [
"lazy_static",
"libc",
"log 0.4.8",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
@ -1943,7 +1920,7 @@ version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
dependencies = [
"memchr 2.3.3",
"memchr",
"version_check 0.1.5",
]
@ -2068,12 +2045,6 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "percent-encoding"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -2182,8 +2153,8 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
dependencies = [
"env_logger 0.7.1",
"log 0.4.8",
"env_logger",
"log",
]
[[package]]
@ -2209,13 +2180,13 @@ dependencies = [
[[package]]
name = "pulldown-cmark"
version = "0.6.1"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c205cc82214f3594e2d50686730314f817c67ffa80fe800cf0db78c3c2b9d9e"
checksum = "3e142c3b8f49d2200605ee6ba0b1d757310e9e7a72afe78c36ee2ef67300ee00"
dependencies = [
"bitflags",
"getopts",
"memchr 2.3.3",
"memchr",
"unicase",
]
@ -2225,17 +2196,6 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quickcheck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c2411d418cea2364325b18a205664f9ef8252e06b2e911db97c0b0d98b1406"
dependencies = [
"env_logger 0.3.5",
"log 0.3.9",
"rand 0.3.23",
]
[[package]]
name = "quote"
version = "1.0.3"
@ -2251,21 +2211,11 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af"
dependencies = [
"log 0.4.8",
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]]
name = "rand"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
dependencies = [
"libc",
"rand 0.4.6",
]
[[package]]
name = "rand"
version = "0.4.6"
@ -2293,7 +2243,7 @@ dependencies = [
"rand_isaac",
"rand_jitter",
"rand_os",
"rand_pcg",
"rand_pcg 0.1.2",
"rand_xorshift",
"winapi 0.3.8",
]
@ -2309,6 +2259,7 @@ dependencies = [
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc 0.2.0",
"rand_pcg 0.2.1",
]
[[package]]
@ -2417,6 +2368,15 @@ dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_xorshift"
version = "0.1.1"
@ -2452,37 +2412,18 @@ dependencies = [
"rust-argon2",
]
[[package]]
name = "regex"
version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
dependencies = [
"aho-corasick 0.5.3",
"memchr 0.1.11",
"regex-syntax 0.3.9",
"thread_local 0.2.7",
"utf8-ranges",
]
[[package]]
name = "regex"
version = "1.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3"
dependencies = [
"aho-corasick 0.7.10",
"memchr 2.3.3",
"regex-syntax 0.6.17",
"thread_local 1.0.1",
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-syntax"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
[[package]]
name = "regex-syntax"
version = "0.6.17"
@ -2523,9 +2464,9 @@ dependencies = [
"hyper",
"hyper-tls",
"lazy_static",
"log 0.4.8",
"log",
"md5",
"percent-encoding 2.1.0",
"percent-encoding",
"pin-project",
"rusoto_credential",
"rusoto_signature",
@ -2549,7 +2490,7 @@ dependencies = [
"futures 0.3.4",
"hyper",
"pin-project",
"regex 1.3.6",
"regex",
"serde",
"serde_json",
"shlex",
@ -2583,9 +2524,9 @@ dependencies = [
"hmac",
"http",
"hyper",
"log 0.4.8",
"log",
"md5",
"percent-encoding 2.1.0",
"percent-encoding",
"pin-project",
"rusoto_credential",
"rustc_version",
@ -2699,21 +2640,24 @@ dependencies = [
[[package]]
name = "seed"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd5d8fd7f12f565f639caf4b6d74a3853d5d7234d0543ec3beae81311492623e"
checksum = "882f4569a394bbb2f15f2fc410e0fbcef178fe24fc2d91599607a598443c6df8"
dependencies = [
"console_error_panic_hook",
"cookie",
"dbg",
"enclose",
"futures 0.3.4",
"gloo-file",
"gloo-timers",
"indexmap",
"js-sys",
"pulldown-cmark",
"rand 0.7.3",
"serde",
"serde_json",
"uuid 0.8.1",
"version_check 0.9.1",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -2737,18 +2681,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.105"
version = "1.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff"
checksum = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.105"
version = "1.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac5d00fc561ba2724df6758a17de23df5914f20e41cb00f94d5b7ae42fffaff8"
checksum = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984"
dependencies = [
"proc-macro2",
"quote",
@ -2757,9 +2701,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.48"
version = "1.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25"
checksum = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2"
dependencies = [
"itoa",
"ryu",
@ -2775,7 +2719,7 @@ dependencies = [
"dtoa",
"itoa",
"serde",
"url 2.1.1",
"url",
]
[[package]]
@ -2970,25 +2914,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "thread-id"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03"
dependencies = [
"kernel32-sys",
"libc",
]
[[package]]
name = "thread_local"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5"
dependencies = [
"thread-id",
]
[[package]]
name = "thread_local"
version = "1.0.1"
@ -3068,7 +2993,7 @@ dependencies = [
"iovec",
"lazy_static",
"libc",
"memchr 2.3.3",
"memchr",
"mio",
"mio-named-pipes",
"mio-uds",
@ -3109,7 +3034,7 @@ dependencies = [
"bytes",
"futures-core",
"futures-sink",
"log 0.4.8",
"log",
"pin-project-lite",
"tokio",
]
@ -3139,14 +3064,14 @@ dependencies = [
"enum-as-inner",
"failure",
"futures 0.3.4",
"idna 0.2.0",
"idna",
"lazy_static",
"log 0.4.8",
"log",
"rand 0.7.3",
"smallvec",
"socket2",
"tokio",
"url 2.1.1",
"url",
]
[[package]]
@ -3160,7 +3085,7 @@ dependencies = [
"futures 0.3.4",
"ipconfig",
"lazy_static",
"log 0.4.8",
"log",
"lru-cache",
"resolv-conf",
"smallvec",
@ -3180,7 +3105,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
dependencies = [
"memchr 2.3.3",
"memchr",
"unchecked-index",
]
@ -3259,34 +3184,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "url"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
dependencies = [
"idna 0.1.5",
"matches",
"percent-encoding 1.0.1",
]
[[package]]
name = "url"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
dependencies = [
"idna 0.2.0",
"idna",
"matches",
"percent-encoding 2.1.0",
"percent-encoding",
]
[[package]]
name = "utf8-ranges"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f"
[[package]]
name = "uuid"
version = "0.6.5"
@ -3388,7 +3296,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log 0.4.8",
"log",
"try-lock",
]
@ -3400,9 +3308,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasm-bindgen"
version = "0.2.60"
version = "0.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f"
checksum = "e3c7d40d09cdbf0f4895ae58cf57d92e1e57a9dd8ed2e8390514b54a47cc5551"
dependencies = [
"cfg-if",
"serde",
@ -3412,13 +3320,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.60"
version = "0.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd"
checksum = "c3972e137ebf830900db522d6c8fd74d1900dcfc733462e9a12e942b00b4ac94"
dependencies = [
"bumpalo",
"lazy_static",
"log 0.4.8",
"log",
"proc-macro2",
"quote",
"syn",
@ -3427,9 +3335,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.10"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7add542ea1ac7fdaa9dc25e031a6af33b7d63376292bd24140c637d00d1c312a"
checksum = "8a369c5e1dfb7569e14d62af4da642a3cbc2f9a3652fe586e26ac22222aa4b04"
dependencies = [
"cfg-if",
"js-sys",
@ -3439,9 +3347,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.60"
version = "0.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4"
checksum = "2cd85aa2c579e8892442954685f0d801f9129de24fa2136b2c6a539c76b65776"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -3449,9 +3357,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.60"
version = "0.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931"
checksum = "8eb197bd3a47553334907ffd2f16507b4f4f01bbec3ac921a7719e0decdfe72a"
dependencies = [
"proc-macro2",
"quote",
@ -3462,9 +3370,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.60"
version = "0.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639"
checksum = "a91c2916119c17a8e316507afaaa2dd94b47646048014bbdf6bef098c1bb58ad"
[[package]]
name = "wasm-bindgen-test"
@ -3492,9 +3400,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.37"
version = "0.3.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb"
checksum = "8bc359e5dd3b46cb9687a051d50a2fdd228e4ba7cf6fcf861a5365c3d671a642"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@ -19,7 +19,7 @@ opt-level = 's'
[dependencies]
jirs-data = { path = "../jirs-data" }
seed = { version = "*" }
seed = { version = "0.7.0" }
serde = "*"
serde_json = "*"
bincode = "1.2.1"

21
jirs-client/jirs.nginx Normal file
View File

@ -0,0 +1,21 @@
server {
listen 80;
server_name jirs.lvh.me;
charset utf-8;
root /home/eraden/code/eraden/jirs/jirs-client/tmp;
location ~ .wasm {
default_type application/wasm;
}
location *.js {
default_type application/javascript;
}
location / {
index index.html index.htm;
}
error_page 404 =200 /index.html;
}

View File

@ -1,11 +1,12 @@
.styledInput {
position: relative;
display: inline-block;
height: 32px;
min-height: 32px;
width: 100%;
}
.styledInput > .inputElement {
min-height: 32px;
height: 100%;
width: 100%;
padding: 0 7px;

View File

@ -2,85 +2,10 @@ const getWsHostName = () => process.env.JIRS_SERVER_BIND === "0.0.0.0" ? 'localh
const getProtocol = () => window.location.protocol.replace(/^http/, 'ws');
const wsUrl = () => `${ getProtocol() }//${ getWsHostName() }:${ process.env.JIRS_SERVER_PORT }/ws/`;
import("../pkg/index.js").then(module => {
let queue = [];
let ws;
const buildWebSocket = () => {
ws = new WebSocket(wsUrl());
ws.binaryType = 'blob';
ws.onopen = event => {
console.log('open', event);
module.reconnected();
};
ws.onerror = event => {
console.error(event);
};
ws.onmessage = async event => {
const arrayBuffer = await event.data.arrayBuffer();
const array = new Uint8Array(arrayBuffer);
module.handle_ws_message(array);
};
ws.onclose = () => {
setTimeout(() => buildWebSocket(), 600);
};
};
buildWebSocket();
window.send_bin_code = code => queue.push(code);
window.inspectQueue = () => queue;
let wsCheckDelay = 100;
const flush = () => {
if (queue.length >= 1000) {
ws.close();
throw new Error("Message queue overflow");
}
// if (queue.length && wsCheckDelay <= 0) console.log(ws.readyState, queue);
switch (ws.readyState) {
case 1: {
const [ code, ...rest ] = queue;
queue = rest;
if (code) {
// console.log('open', code);
ws.send(Uint8Array.from(code).buffer);
}
break;
}
default:
break;
}
window.requestAnimationFrame(flush);
};
window.flush = flush;
const keepWsOpen = () => {
if (wsCheckDelay > 0) {
wsCheckDelay -= 1;
} else {
wsCheckDelay = 100;
switch (ws.readyState) {
case 1: {
// console.log('sending ping');
// ws.send(Uint8Array.from([ 0, 0, 0, 0 ]).buffer);
break;
}
case 0:
case 2:
break;
case 3:
throw new Error('web socket has been closed');
buildWebSocket();
break;
}
}
window.requestAnimationFrame(keepWsOpen);
};
keepWsOpen();
flush();
import("/jirs.js").then(async module => {
window.module = module;
console.log(module)
await module.default();
const host_url = `${ location.protocol }//${ process.env.JIRS_SERVER_BIND }:${ process.env.JIRS_SERVER_PORT }`;
module.set_host_url(host_url);
module.render();
module.render(host_url, wsUrl());
});

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.png">
<link rel="icon" href="/logo.svg">
<title>JIRS</title>
<link rel="stylesheet" type="text/css" href="/styles.css">
</head>
<body>
<main id="app"></main>
<script type="module" src="/index.js"></script>
</body>
</html>

16
jirs-client/scripts/dev.sh Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
. .env
rm -Rf tmp
mkdir tmp
wasm-pack build --mode normal --dev --out-name jirs --out-dir ./tmp --target web
../target/debug/jirs-css -i ./js/styles.css -O ./tmp/styles.css
cat ./js/index.js \
| sed -e "s/process.env.JIRS_SERVER_BIND/'$JIRS_SERVER_BIND'/g" \
| sed -e "s/process.env.JIRS_SERVER_PORT/'$JIRS_SERVER_PORT'/g" &> ./tmp/index.js
cp ./js/template.html ./tmp/index.html
cp -r ./dev/* ./tmp

View File

@ -1,10 +0,0 @@
use jirs_data::*;
pub fn send_ws_msg(msg: WsMsg) {
use crate::send_bin_code;
use wasm_bindgen::JsValue;
let binary = bincode::serialize(&msg).unwrap();
let data = JsValue::from_serde(&binary).unwrap();
send_bin_code(data);
}

View File

@ -0,0 +1,67 @@
use crate::FieldId;
use jirs_data::{IssueId, IssueStatusId};
use crate::shared::styled_editor::Mode as TabMode;
use seed::prelude::WebSocketMessage;
#[derive(Clone, Debug, PartialEq)]
pub enum FieldChange {
LinkCopied(FieldId, bool),
TabChanged(FieldId, TabMode),
ToggleCommentForm(FieldId, bool),
EditComment(FieldId, i32),
}
#[derive(Clone, Debug, PartialEq)]
pub enum BoardPageChange {
// dragging
IssueDragStarted(IssueId),
IssueDragStopped(IssueId),
DragLeave(IssueId),
ExchangePosition(IssueId),
IssueDragOverStatus(IssueStatusId),
IssueDropZone(IssueStatusId),
}
#[derive(Clone, Debug, PartialEq)]
pub enum UsersPageChange {
ResetForm,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ProjectPageChange {
ResetForm,
SubmitForm,
// dragging
ColumnDragStarted(IssueStatusId),
ColumnDragStopped(IssueStatusId),
ColumnDragLeave(IssueStatusId),
ColumnExchangePosition(IssueStatusId),
ColumnDragOverStatus(IssueStatusId),
ColumnDropZone(IssueStatusId),
// edit issue status name
EditIssueStatusName(Option<IssueStatusId>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum ProfilePageChange {
SubmitForm,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PageChanged {
Users(UsersPageChange),
ProjectSettings(ProjectPageChange),
Profile(ProfilePageChange),
Board(BoardPageChange),
}
#[derive(Debug)]
pub enum WebSocketChanged {
WsMsg(jirs_data::WsMsg),
WebSocketMessage(WebSocketMessage),
WebSocketMessageLoaded(Vec<u8>),
WebSocketOpened,
WebSocketClosed,
SendPing,
}

121
jirs-client/src/fields.rs Normal file
View File

@ -0,0 +1,121 @@
use jirs_data::{
CommentFieldId, InviteFieldId, IssueFieldId, ProjectFieldId, SignInFieldId, SignUpFieldId,
UsersFieldId,
};
pub type AvatarFilterActive = bool;
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum EditIssueModalSection {
Issue(IssueFieldId),
Comment(CommentFieldId),
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum FieldId {
SignIn(SignInFieldId),
SignUp(SignUpFieldId),
Invite(InviteFieldId),
Users(UsersFieldId),
Profile(UsersFieldId),
// issue
AddIssueModal(IssueFieldId),
EditIssueModal(EditIssueModalSection),
// project boards
TextFilterBoard,
CopyButtonLabel,
ProjectSettings(ProjectFieldId),
}
impl std::fmt::Display for FieldId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FieldId::EditIssueModal(sub) => match sub {
EditIssueModalSection::Issue(IssueFieldId::Type) => {
f.write_str("issueTypeEditModalTop")
}
EditIssueModalSection::Issue(IssueFieldId::Title) => {
f.write_str("titleIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::Description) => {
f.write_str("descriptionIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::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::TimeRemaining) => {
f.write_str("timeRemainingIssueEditModal")
}
EditIssueModalSection::Comment(CommentFieldId::Body) => {
f.write_str("editIssue-commentBody")
}
EditIssueModalSection::Issue(IssueFieldId::ListPosition) => {
f.write_str("editIssue-listPosition")
}
},
FieldId::AddIssueModal(sub) => match sub {
IssueFieldId::Type => f.write_str("issueTypeAddIssueModal"),
IssueFieldId::Title => f.write_str("summaryAddIssueModal"),
IssueFieldId::Description => f.write_str("descriptionAddIssueModal"),
IssueFieldId::Reporter => f.write_str("reporterAddIssueModal"),
IssueFieldId::Assignees => f.write_str("assigneesAddIssueModal"),
IssueFieldId::Priority => f.write_str("issuePriorityAddIssueModal"),
IssueFieldId::IssueStatusId => f.write_str("addIssueModal-status"),
IssueFieldId::Estimate => f.write_str("addIssueModal-estimate"),
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"),
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"),
},
FieldId::TextFilterBoard => f.write_str("textFilterBoard"),
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"),
FieldId::ProjectSettings(sub) => match sub {
ProjectFieldId::Name => f.write_str("projectSettings-name"),
ProjectFieldId::Url => f.write_str("projectSettings-url"),
ProjectFieldId::Description => f.write_str("projectSettings-description"),
ProjectFieldId::Category => f.write_str("projectSettings-category"),
ProjectFieldId::TimeTracking => f.write_str("projectSettings-timeTracking"),
ProjectFieldId::IssueStatusName => f.write_str("projectSettings-issueStatusName"),
},
FieldId::SignIn(sub) => match sub {
SignInFieldId::Email => f.write_str("login-email"),
SignInFieldId::Username => f.write_str("login-username"),
SignInFieldId::Token => f.write_str("login-token"),
},
FieldId::SignUp(sub) => match sub {
SignUpFieldId::Username => f.write_str("signUp-email"),
SignUpFieldId::Email => f.write_str("signUp-username"),
},
FieldId::Invite(sub) => match sub {
InviteFieldId::Token => f.write_str("invite-token"),
},
FieldId::Users(sub) => match sub {
UsersFieldId::Username => f.write_str("users-username"),
UsersFieldId::Email => f.write_str("users-email"),
UsersFieldId::UserRole => f.write_str("users-userRole"),
UsersFieldId::Avatar => f.write_str("users-avatar"),
},
FieldId::Profile(sub) => match sub {
UsersFieldId::Username => f.write_str("profile-username"),
UsersFieldId::Email => f.write_str("profile-email"),
UsersFieldId::UserRole => f.write_str("profile-userRole"),
UsersFieldId::Avatar => f.write_str("profile-avatar"),
},
}
}
}

View File

@ -1,16 +1,15 @@
use std::sync::RwLock;
use seed::{prelude::*, *};
use web_sys::File;
use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::{ModalType, Model, Page};
use crate::shared::styled_editor::Mode as TabMode;
use crate::shared::styled_select::StyledSelectChange;
use crate::shared::{go_to_board, go_to_login};
use crate::ws::{open_socket, read_incoming, send_ws_msg};
mod api;
mod changes;
mod fields;
mod invite;
mod modal;
mod model;
@ -24,179 +23,13 @@ mod users;
pub mod validations;
mod ws;
pub type AvatarFilterActive = bool;
pub use changes::*;
pub use fields::*;
pub type AppType = App<Msg, Model, Node<Msg>>;
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum EditIssueModalSection {
Issue(IssueFieldId),
Comment(CommentFieldId),
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum FieldId {
SignIn(SignInFieldId),
SignUp(SignUpFieldId),
Invite(InviteFieldId),
Users(UsersFieldId),
Profile(UsersFieldId),
// issue
AddIssueModal(IssueFieldId),
EditIssueModal(EditIssueModalSection),
// project boards
TextFilterBoard,
CopyButtonLabel,
ProjectSettings(ProjectFieldId),
}
impl std::fmt::Display for FieldId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FieldId::EditIssueModal(sub) => match sub {
EditIssueModalSection::Issue(IssueFieldId::Type) => {
f.write_str("issueTypeEditModalTop")
}
EditIssueModalSection::Issue(IssueFieldId::Title) => {
f.write_str("titleIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::Description) => {
f.write_str("descriptionIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::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::TimeRemaining) => {
f.write_str("timeRemainingIssueEditModal")
}
EditIssueModalSection::Comment(CommentFieldId::Body) => {
f.write_str("editIssue-commentBody")
}
EditIssueModalSection::Issue(IssueFieldId::ListPosition) => {
f.write_str("editIssue-listPosition")
}
},
FieldId::AddIssueModal(sub) => match sub {
IssueFieldId::Type => f.write_str("issueTypeAddIssueModal"),
IssueFieldId::Title => f.write_str("summaryAddIssueModal"),
IssueFieldId::Description => f.write_str("descriptionAddIssueModal"),
IssueFieldId::Reporter => f.write_str("reporterAddIssueModal"),
IssueFieldId::Assignees => f.write_str("assigneesAddIssueModal"),
IssueFieldId::Priority => f.write_str("issuePriorityAddIssueModal"),
IssueFieldId::IssueStatusId => f.write_str("addIssueModal-status"),
IssueFieldId::Estimate => f.write_str("addIssueModal-estimate"),
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"),
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"),
},
FieldId::TextFilterBoard => f.write_str("textFilterBoard"),
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"),
FieldId::ProjectSettings(sub) => match sub {
ProjectFieldId::Name => f.write_str("projectSettings-name"),
ProjectFieldId::Url => f.write_str("projectSettings-url"),
ProjectFieldId::Description => f.write_str("projectSettings-description"),
ProjectFieldId::Category => f.write_str("projectSettings-category"),
ProjectFieldId::TimeTracking => f.write_str("projectSettings-timeTracking"),
ProjectFieldId::IssueStatusName => f.write_str("projectSettings-issueStatusName"),
},
FieldId::SignIn(sub) => match sub {
SignInFieldId::Email => f.write_str("login-email"),
SignInFieldId::Username => f.write_str("login-username"),
SignInFieldId::Token => f.write_str("login-token"),
},
FieldId::SignUp(sub) => match sub {
SignUpFieldId::Username => f.write_str("signUp-email"),
SignUpFieldId::Email => f.write_str("signUp-username"),
},
FieldId::Invite(sub) => match sub {
InviteFieldId::Token => f.write_str("invite-token"),
},
FieldId::Users(sub) => match sub {
UsersFieldId::Username => f.write_str("users-username"),
UsersFieldId::Email => f.write_str("users-email"),
UsersFieldId::UserRole => f.write_str("users-userRole"),
UsersFieldId::Avatar => f.write_str("users-avatar"),
},
FieldId::Profile(sub) => match sub {
UsersFieldId::Username => f.write_str("profile-username"),
UsersFieldId::Email => f.write_str("profile-email"),
UsersFieldId::UserRole => f.write_str("profile-userRole"),
UsersFieldId::Avatar => f.write_str("profile-avatar"),
},
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum FieldChange {
LinkCopied(FieldId, bool),
TabChanged(FieldId, TabMode),
ToggleCommentForm(FieldId, bool),
EditComment(FieldId, i32),
}
#[derive(Clone, Debug, PartialEq)]
pub enum BoardPageChange {
// dragging
IssueDragStarted(IssueId),
IssueDragStopped(IssueId),
DragLeave(IssueId),
ExchangePosition(IssueId),
IssueDragOverStatus(IssueStatusId),
IssueDropZone(IssueStatusId),
}
#[derive(Clone, Debug, PartialEq)]
pub enum UsersPageChange {
ResetForm,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ProjectPageChange {
ResetForm,
SubmitForm,
// dragging
ColumnDragStarted(IssueStatusId),
ColumnDragStopped(IssueStatusId),
ColumnDragLeave(IssueStatusId),
ColumnExchangePosition(IssueStatusId),
ColumnDragOverStatus(IssueStatusId),
ColumnDropZone(IssueStatusId),
// edit issue status name
EditIssueStatusName(Option<IssueStatusId>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum ProfilePageChange {
SubmitForm,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PageChanged {
Users(UsersPageChange),
ProjectSettings(ProjectPageChange),
Profile(ProfilePageChange),
Board(BoardPageChange),
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub enum Msg {
NoOp,
GlobalKeyDown {
key: String,
shift: bool,
@ -248,35 +81,82 @@ pub enum Msg {
DeleteComment(CommentId),
// profile
AvatarUpdateFetched(seed::fetch::FetchObject<String>),
AvatarUpdateFetched(String),
// modals
ModalOpened(Box<ModalType>),
ModalDropped,
ModalChanged(FieldChange),
WsMsg(jirs_data::WsMsg),
// WebSocket
WebSocketChange(WebSocketChanged),
}
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::NoOp => return,
_ => (),
if model.ws.is_none() {
open_socket(model, orders);
}
let msg = match msg {
Msg::WebSocketChange(change) => {
match change {
WebSocketChanged::WebSocketOpened => {
authorize_or_redirect(model);
send_ws_msg(WsMsg::Ping, model.ws.as_ref());
return;
}
WebSocketChanged::SendPing => {
send_ws_msg(WsMsg::Ping, model.ws.as_ref());
return;
}
WebSocketChanged::WebSocketMessage(incoming) => {
orders.perform_cmd(read_incoming(incoming));
return;
}
WebSocketChanged::WsMsg(ref ws_msg) => {
ws::update(ws_msg, model, orders);
}
WebSocketChanged::WebSocketMessageLoaded(v) => {
if let Ok(m) = bincode::deserialize(v.clone().as_slice()) {
match m {
WsMsg::Ping | WsMsg::Pong => {
orders.perform_cmd(cmds::timeout(1000, || {
Msg::WebSocketChange(WebSocketChanged::SendPing)
}));
}
_ => {
orders
.skip()
.send_msg(Msg::WebSocketChange(WebSocketChanged::WsMsg(m)));
}
}
};
return;
}
WebSocketChanged::WebSocketClosed => {
open_socket(model, orders);
}
};
Msg::WebSocketChange(change)
}
_ => msg,
};
if cfg!(debug_assertions) {
log!(msg);
}
match &msg {
Msg::AuthTokenStored => {
seed::push_route(vec!["dashboard"]);
go_to_board();
orders.skip().send_msg(Msg::ChangePage(Page::Project));
authorize_or_redirect();
authorize_or_redirect(model);
return;
}
Msg::AuthTokenErased => {
seed::push_route(vec!["login"]);
go_to_login();
orders.skip().send_msg(Msg::ChangePage(Page::SignIn));
authorize_or_redirect();
authorize_or_redirect(model);
return;
}
Msg::ChangePage(page) => {
@ -287,7 +167,6 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
}
_ => (),
}
crate::ws::update(&msg, model, orders);
crate::modal::update(&msg, model, orders);
match model.page {
Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders),
@ -317,116 +196,98 @@ fn view(model: &model::Model) -> Node<Msg> {
}
fn routes(url: Url) -> Option<Msg> {
if url.path.is_empty() {
return Some(Msg::ChangePage(model::Page::Project));
match resolve_page(url) {
Some(page) => Some(Msg::ChangePage(page)),
_ => None,
}
}
fn resolve_page(url: Url) -> Option<Page> {
if url.path().is_empty() {
return Some(Page::Project);
}
match url.path[0].as_ref() {
"board" => Some(Msg::ChangePage(model::Page::Project)),
"issues" => match url.path.get(1).as_ref().map(|s| s.parse::<i32>()) {
Some(Ok(id)) => Some(Msg::ChangePage(model::Page::EditIssue(id))),
_ => None,
let page = match url.path()[0].as_ref() {
"board" => Page::Project,
"issues" => match url.path().get(1).as_ref().map(|s| s.parse::<i32>()) {
Some(Ok(id)) => Page::EditIssue(id),
_ => return None,
},
"profile" => Some(Msg::ChangePage(Page::Profile)),
"add-issue" => Some(Msg::ChangePage(Page::AddIssue)),
"project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)),
"login" => Some(Msg::ChangePage(model::Page::SignIn)),
"register" => Some(Msg::ChangePage(model::Page::SignUp)),
"invite" => Some(Msg::ChangePage(model::Page::Invite)),
"users" => Some(Msg::ChangePage(model::Page::Users)),
_ => Some(Msg::ChangePage(model::Page::Project)),
}
"profile" => Page::Profile,
"add-issue" => Page::AddIssue,
"project-settings" => Page::ProjectSettings,
"login" => Page::SignIn,
"register" => Page::SignUp,
"invite" => Page::Invite,
"users" => Page::Users,
_ => Page::Project,
};
Some(page)
}
pub static mut HOST_URL: String = String::new();
pub static mut APP: Option<RwLock<App<Msg, Model, Node<Msg>>>> = None;
pub static mut WS_URL: String = String::new();
#[wasm_bindgen]
pub fn set_host_url(url: String) {
pub fn render(host_url: String, ws_url: String) {
unsafe {
HOST_URL = url;
}
}
#[wasm_bindgen]
pub fn handle_ws_message(value: &wasm_bindgen::JsValue) {
let a = js_sys::Uint8Array::new(value);
let mut v = Vec::new();
for idx in 0..a.length() {
v.push(a.get_index(idx));
}
if let Ok(msg) = bincode::deserialize(v.as_slice()) {
ws::handle(msg);
};
}
#[wasm_bindgen]
pub fn reconnected() {
authorize_or_redirect();
}
#[wasm_bindgen]
extern "C" {
pub fn send_bin_code(data: wasm_bindgen::JsValue);
}
#[wasm_bindgen]
pub fn render() {
seed::set_interval(
Box::new(|| {
let binary = bincode::serialize(&jirs_data::WsMsg::Ping).unwrap();
let data = JsValue::from_serde(&binary).unwrap();
send_bin_code(data);
}) as Box<dyn Fn()>,
5000,
);
if let Some(body) = seed::html_document().body() {
use wasm_bindgen::JsCast;
let body = body.dyn_ref::<web_sys::HtmlBodyElement>().unwrap().clone();
let key_up_closure =
wasm_bindgen::closure::Closure::wrap(Box::new(|event: web_sys::KeyboardEvent| {
if let Some(Ok(app)) = unsafe { APP.as_mut().map(|app| app.write()) } {
let msg = Msg::GlobalKeyDown {
key: event.key(),
shift: event.shift_key(),
ctrl: event.ctrl_key(),
alt: event.alt_key(),
};
app.update(msg);
}
})
as Box<dyn Fn(web_sys::KeyboardEvent)>);
body.add_event_listener_with_callback("keyup", key_up_closure.as_ref().unchecked_ref())
.unwrap();
key_up_closure.forget();
HOST_URL = host_url;
WS_URL = ws_url;
}
let app = seed::App::builder(update, view)
// if let Some(body) = seed::html_document().body() {
// use wasm_bindgen::JsCast;
// let body = body.dyn_ref::<web_sys::HtmlBodyElement>().unwrap().clone();
// let key_up_closure =
// wasm_bindgen::closure::Closure::wrap(Box::new(|event: web_sys::KeyboardEvent| {
// if let Some(Ok(app)) = unsafe { APP.as_mut().map(|app| app.write()) } {
// let msg = Msg::GlobalKeyDown {
// key: event.key(),
// shift: event.shift_key(),
// ctrl: event.ctrl_key(),
// alt: event.alt_key(),
// };
// app.update(msg);
// }
// })
// as Box<dyn Fn(web_sys::KeyboardEvent)>);
// body.add_event_listener_with_callback("keyup", key_up_closure.as_ref().unchecked_ref())
// .unwrap();
// key_up_closure.forget();
// }
let _app = seed::App::builder(update, view)
.routes(routes)
.after_mount(after_mount)
.build_and_start();
}
authorize_or_redirect();
let cell_app = std::sync::RwLock::new(app);
fn after_mount(url: Url, orders: &mut impl Orders<Msg>) -> AfterMount<Model> {
let host_url = unsafe { HOST_URL.clone() };
let ws_url = unsafe { WS_URL.clone() };
let mut model = Model::new(host_url, ws_url);
unsafe {
APP = Some(cell_app);
};
HOST_URL = "".to_string();
WS_URL = "".to_string();
}
model.page = resolve_page(url).unwrap_or_else(|| Page::Project);
log!(model);
open_socket(&mut model, orders);
AfterMount::new(model).url_handling(UrlHandling::PassToRoutes)
}
#[inline]
fn authorize_or_redirect() {
fn authorize_or_redirect(model: &Model) {
match crate::shared::read_auth_token() {
Ok(token) => {
send_ws_msg(WsMsg::AuthorizeRequest(token));
send_ws_msg(WsMsg::AuthorizeRequest(token), model.ws.as_ref());
}
Err(..) => {
let pathname = seed::document().location().unwrap().pathname().unwrap();
match pathname.as_str() {
"/login" | "/register" | "/invite" => {}
_ => {
seed::push_route(vec!["login"]);
go_to_login();
}
};
}

View File

@ -1,9 +1,8 @@
use seed::{prelude::*, *};
use jirs_data::IssueFieldId;
use jirs_data::{IssueFieldId, WsMsg};
use jirs_data::{IssuePriority, IssueType, ToVec};
use crate::api::send_ws_msg;
use crate::model::{AddIssueModal, ModalType, Model};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
@ -14,7 +13,8 @@ use crate::shared::styled_select::StyledSelect;
use crate::shared::styled_select::StyledSelectChange;
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::{ToChild, ToNode};
use crate::{FieldId, Msg};
use crate::ws::send_ws_msg;
use crate::{FieldId, Msg, WebSocketChanged};
pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
let modal = model.modals.iter_mut().find(|modal| match modal {
@ -50,9 +50,12 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
user_ids: modal.user_ids.clone(),
reporter_id: modal.reporter_id.unwrap_or_else(|| user_id),
};
send_ws_msg(jirs_data::WsMsg::IssueCreateRequest(payload));
send_ws_msg(
jirs_data::WsMsg::IssueCreateRequest(payload),
model.ws.as_ref(),
);
}
Msg::WsMsg(jirs_data::WsMsg::IssueCreated(issue)) => {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueCreated(issue))) => {
model.issues.push(issue.clone());
orders.skip().send_msg(Msg::ModalDropped);
}

View File

@ -2,11 +2,10 @@ use seed::prelude::*;
use jirs_data::{IssueStatusId, WsMsg};
use crate::api::send_ws_msg;
use crate::model::{DeleteIssueStatusModal, ModalType, Model};
use crate::shared::styled_confirm_modal::StyledConfirmModal;
use crate::shared::ToNode;
use crate::{model, Msg};
use crate::{model, Msg, WebSocketChanged};
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let _modal: &mut Box<DeleteIssueStatusModal> =
@ -20,9 +19,12 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::DeleteIssueStatus(issue_status_id) => {
send_ws_msg(WsMsg::IssueStatusDelete(*issue_status_id));
crate::ws::send_ws_msg(
WsMsg::IssueStatusDelete(*issue_status_id),
model.ws.as_ref(),
);
}
Msg::WsMsg(WsMsg::IssueStatusDelete(_)) => {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueStatusDelete(_))) => {
orders.skip().send_msg(Msg::ModalDropped);
}
_ => (),

View File

@ -2,7 +2,6 @@ use seed::{prelude::*, *};
use jirs_data::*;
use crate::api::send_ws_msg;
use crate::modal::time_tracking::time_tracking_field;
use crate::model::{CommentForm, EditIssueModal, ModalType, Model};
use crate::shared::styled_avatar::StyledAvatar;
@ -15,7 +14,8 @@ use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::tracking_widget::tracking_link;
use crate::shared::{ToChild, ToNode};
use crate::{EditIssueModalSection, FieldChange, FieldId, Msg};
use crate::ws::send_ws_msg;
use crate::{EditIssueModalSection, FieldChange, FieldId, Msg, WebSocketChanged};
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let modal: &mut EditIssueModal = match model.modals.get_mut(0) {
@ -35,7 +35,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.time_remaining_select.update(msg, orders);
match msg {
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
modal.payload = issue.clone().into();
}
Msg::StyledSelectChanged(
@ -43,44 +43,56 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
StyledSelectChange::Changed(value),
) => {
modal.payload.issue_type = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Type,
PayloadVariant::IssueType(modal.payload.issue_type),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Type,
PayloadVariant::IssueType(modal.payload.issue_type),
),
model.ws.as_ref(),
);
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
StyledSelectChange::Changed(value),
) => {
modal.payload.issue_status_id = *value as IssueStatusId;
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::IssueStatusId,
PayloadVariant::I32(modal.payload.issue_status_id),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::IssueStatusId,
PayloadVariant::I32(modal.payload.issue_status_id),
),
model.ws.as_ref(),
);
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
StyledSelectChange::Changed(value),
) => {
modal.payload.reporter_id = *value as i32;
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Reporter,
PayloadVariant::I32(modal.payload.reporter_id),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Reporter,
PayloadVariant::I32(modal.payload.reporter_id),
),
model.ws.as_ref(),
);
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
StyledSelectChange::Changed(value),
) => {
modal.payload.user_ids.push(*value as i32);
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Assignees,
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Assignees,
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
),
model.ws.as_ref(),
);
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
@ -94,33 +106,42 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.payload.user_ids.push(id);
}
}
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Assignees,
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Assignees,
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
),
model.ws.as_ref(),
);
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
StyledSelectChange::Changed(value),
) => {
modal.payload.priority = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Priority,
PayloadVariant::IssuePriority(modal.payload.priority),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Priority,
PayloadVariant::IssuePriority(modal.payload.priority),
),
model.ws.as_ref(),
);
}
Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
value,
) => {
modal.payload.title = value.clone();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Title,
PayloadVariant::String(modal.payload.title.clone()),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Title,
PayloadVariant::String(modal.payload.title.clone()),
),
model.ws.as_ref(),
);
}
Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
@ -128,18 +149,21 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.description = Some(value.clone());
modal.payload.description_text = Some(value.clone());
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Description,
PayloadVariant::String(
modal
.payload
.description
.as_ref()
.cloned()
.unwrap_or_default(),
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Description,
PayloadVariant::String(
modal
.payload
.description
.as_ref()
.cloned()
.unwrap_or_default(),
),
),
));
model.ws.as_ref(),
);
}
// TimeSpent
Msg::StrInputChanged(
@ -147,22 +171,28 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
..,
) => {
modal.payload.time_spent = modal.time_spent.represent_f64_as_i32();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeSpent,
PayloadVariant::OptionI32(modal.payload.time_spent),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeSpent,
PayloadVariant::OptionI32(modal.payload.time_spent),
),
model.ws.as_ref(),
);
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
StyledSelectChange::Changed(..),
) => {
modal.payload.time_spent = modal.time_spent_select.values.get(0).map(|n| *n as i32);
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeSpent,
PayloadVariant::OptionI32(modal.payload.time_spent),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeSpent,
PayloadVariant::OptionI32(modal.payload.time_spent),
),
model.ws.as_ref(),
);
}
// Time Remaining
Msg::StrInputChanged(
@ -170,11 +200,14 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
..,
) => {
modal.payload.time_remaining = modal.time_remaining.represent_f64_as_i32();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.time_remaining),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.time_remaining),
),
model.ws.as_ref(),
);
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
@ -182,11 +215,14 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.time_remaining =
modal.time_remaining_select.values.get(0).map(|n| *n as i32);
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.time_remaining),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.time_remaining),
),
model.ws.as_ref(),
);
}
// Estimate
Msg::StrInputChanged(
@ -194,22 +230,28 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
..,
) => {
modal.payload.estimate = modal.estimate.represent_f64_as_i32();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Estimate,
PayloadVariant::OptionI32(modal.payload.estimate),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Estimate,
PayloadVariant::OptionI32(modal.payload.estimate),
),
model.ws.as_ref(),
);
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
StyledSelectChange::Changed(..),
) => {
modal.payload.estimate = modal.estimate_select.values.get(0).map(|n| *n as i32);
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Estimate,
PayloadVariant::OptionI32(modal.payload.estimate),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::Estimate,
PayloadVariant::OptionI32(modal.payload.estimate),
),
model.ws.as_ref(),
);
}
Msg::ModalChanged(FieldChange::TabChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
@ -248,7 +290,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
issue_id: modal.id,
}),
};
send_ws_msg(msg);
send_ws_msg(msg, model.ws.as_ref());
orders
.skip()
.send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm(
@ -272,7 +314,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.comment_form.creating = true;
}
Msg::DeleteComment(comment_id) => {
send_ws_msg(WsMsg::CommentDeleteRequest(*comment_id));
send_ws_msg(WsMsg::CommentDeleteRequest(*comment_id), model.ws.as_ref());
orders.skip().send_msg(Msg::ModalDropped);
}
@ -314,15 +356,13 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let issue_id = *id;
let click_handler = mouse_ev(Ev::Click, move |_| {
use wasm_bindgen::JsCast;
let link = format!("http://localhost:7000/issues/{id}", id = issue_id);
let el = match seed::html_document().create_element("textarea") {
Ok(el) => el
.dyn_ref::<web_sys::HtmlTextAreaElement>()
.unwrap()
.clone(),
_ => return Msg::NoOp,
_ => return None as Option<Msg>,
};
seed::body().append_child(&el).unwrap();
el.set_text_content(Some(link.as_str()));
@ -330,7 +370,10 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
el.set_selection_range(0, 9999).unwrap();
seed::html_document().exec_command("copy").unwrap();
seed::body().remove_child(&el).unwrap();
Msg::ModalChanged(FieldChange::LinkCopied(FieldId::CopyButtonLabel, true))
Some(Msg::ModalChanged(FieldChange::LinkCopied(
FieldId::CopyButtonLabel,
true,
)))
});
let close_handler = mouse_ev(Ev::Click, |_| Msg::ModalDropped);
let delete_confirmation_handler = mouse_ev(Ev::Click, move |_| {
@ -579,7 +622,7 @@ fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Option<N
div![
class!["content"],
div![class!["userName"], user.name.as_str()],
p![class!["body"], comment.body],
p![class!["body"], comment.body.as_str()],
buttons,
]
};

View File

@ -2,12 +2,12 @@ use seed::{prelude::*, *};
use jirs_data::{TimeTracking, WsMsg};
use crate::api::send_ws_msg;
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page};
use crate::shared::styled_confirm_modal::StyledConfirmModal;
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
use crate::shared::{find_issue, ToNode};
use crate::{model, FieldChange, FieldId, Msg};
use crate::shared::{find_issue, go_to_board, ToNode};
use crate::ws::send_ws_msg;
use crate::{model, FieldChange, FieldId, Msg, WebSocketChanged};
mod add_issue;
mod confirm_delete_issue;
@ -19,7 +19,7 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
match msg {
Msg::ModalDropped => match model.modals.pop() {
Some(ModalType::EditIssue(..)) | Some(ModalType::AddIssue(..)) => {
seed::push_route(vec!["board"]);
go_to_board();
orders.send_msg(Msg::ChangePage(Page::Project));
}
_ => (),
@ -37,12 +37,14 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
model.modals.push(modal_type.as_ref().clone());
}
Msg::WsMsg(jirs_data::WsMsg::ProjectIssuesLoaded(_issues)) => match model.page {
Page::EditIssue(issue_id) if model.modals.is_empty() => {
push_edit_modal(issue_id, model)
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::ProjectIssuesLoaded(_issues))) => {
match model.page {
Page::EditIssue(issue_id) if model.modals.is_empty() => {
push_edit_modal(issue_id, model)
}
_ => (),
}
_ => (),
},
}
Msg::ChangePage(Page::EditIssue(issue_id)) => {
push_edit_modal(*issue_id, model);
@ -116,6 +118,6 @@ fn push_edit_modal(issue_id: i32, model: &mut Model) {
Box::new(EditIssueModal::new(issue, time_tracking_type)),
)
};
send_ws_msg(WsMsg::IssueCommentsRequest(issue_id));
send_ws_msg(WsMsg::IssueCommentsRequest(issue_id), model.ws.as_ref());
model.modals.push(modal);
}

View File

@ -12,7 +12,8 @@ use crate::shared::styled_editor::Mode;
use crate::shared::styled_image_input::StyledImageInputState;
use crate::shared::styled_input::StyledInputState;
use crate::shared::styled_select::StyledSelectState;
use crate::{EditIssueModalSection, FieldId, ProjectFieldId, HOST_URL};
use crate::{EditIssueModalSection, FieldId, ProjectFieldId /*HOST_URL*/};
use seed::browser::web_socket::WebSocket;
#[derive(Clone, Debug, PartialOrd, PartialEq)]
pub enum ModalType {
@ -269,6 +270,7 @@ pub struct ProjectSettingsPage {
pub time_tracking: StyledCheckboxState,
pub column_drag: DragState,
pub edit_column_id: Option<IssueStatusId>,
pub creating_issue_status: bool,
pub name: StyledInputState,
}
@ -304,6 +306,7 @@ impl ProjectSettingsPage {
),
column_drag: Default::default(),
edit_column_id: None,
creating_issue_status: false,
name: StyledInputState::new(
FieldId::ProjectSettings(ProjectFieldId::IssueStatusName),
"",
@ -424,7 +427,9 @@ pub enum PageContent {
#[derive(Debug)]
pub struct Model {
pub ws: Option<WebSocket>,
pub host_url: String,
pub ws_url: String,
pub access_token: Option<Uuid>,
pub about_tooltip_visible: bool,
@ -451,10 +456,10 @@ pub struct Model {
pub issue_statuses: Vec<IssueStatus>,
}
impl Default for Model {
fn default() -> Self {
let host_url = unsafe { HOST_URL.clone() };
impl Model {
pub fn new(host_url: String, ws_url: String) -> Self {
Self {
ws: None,
access_token: None,
user: None,
issue_form: None,
@ -465,6 +470,7 @@ impl Default for Model {
comments_by_project_id: Default::default(),
page: Page::Project,
host_url,
ws_url,
page_content: PageContent::Project(Box::new(ProjectPage::default())),
modals: vec![],
project: None,

View File

@ -3,7 +3,6 @@ use web_sys::FormData;
use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::{Model, Page, PageContent, ProfilePage};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
@ -11,7 +10,8 @@ use crate::shared::styled_form::StyledForm;
use crate::shared::styled_image_input::StyledImageInput;
use crate::shared::styled_input::StyledInput;
use crate::shared::{inner_layout, ToNode};
use crate::{FieldId, Msg, PageChanged, ProfilePageChange, HOST_URL};
use crate::ws::send_ws_msg;
use crate::{FieldId, Msg, PageChanged, ProfilePageChange, WebSocketChanged};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
let user = match model.user {
@ -20,8 +20,9 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
};
match msg {
Msg::WsMsg(WsMsg::AuthorizeLoaded(..)) | Msg::ChangePage(Page::Profile) => {
send_ws_msg(WsMsg::ProjectRequest);
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
| Msg::ChangePage(Page::Profile) => {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
model.page_content = PageContent::Profile(Box::new(ProfilePage::new(user)));
}
_ => (),
@ -50,10 +51,13 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
fd.set_with_str("token", format!("{}", token).as_str())
.unwrap();
fd.set_with_blob("avatar", file).unwrap();
orders.perform_cmd(update_avatar(fd));
orders.perform_cmd(update_avatar(fd, model.host_url.clone()));
orders.skip();
}
Msg::WsMsg(WsMsg::AvatarUrlChanged(user_id, avatar_url)) => {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AvatarUrlChanged(
user_id,
avatar_url,
))) => {
if let Some(me) = model.user.as_mut() {
if me.id == user_id {
profile_page.avatar.url = Some(avatar_url.clone());
@ -61,10 +65,13 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
}
}
Msg::PageChanged(PageChanged::Profile(ProfilePageChange::SubmitForm)) => {
send_ws_msg(WsMsg::ProfileUpdate(
profile_page.email.value.clone(),
profile_page.name.value.clone(),
))
send_ws_msg(
WsMsg::ProfileUpdate(
profile_page.email.value.clone(),
profile_page.name.value.clone(),
),
model.ws.as_ref(),
);
}
_ => (),
}
@ -132,12 +139,17 @@ pub fn view(model: &Model) -> Node<Msg> {
inner_layout(model, "profile", vec![content], empty![])
}
async fn update_avatar(data: FormData) -> Result<Msg, Msg> {
let host_url = unsafe { HOST_URL.clone() };
async fn update_avatar(data: FormData, host_url: String) -> Option<Msg> {
let path = format!("{}/avatar/", host_url);
Request::new(path)
let result = Request::new(path)
.method(Method::Post)
.body(data.into())
.fetch_string(Msg::AvatarUpdateFetched)
.await
.fetch()
.await;
let response = match result {
Ok(r) => r,
Err(_) => return None,
};
let text = response.text().await.ok()?;
Some(Msg::AvatarUpdateFetched(text))
}

View File

@ -3,15 +3,15 @@ use seed::{prelude::*, *};
use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::{ModalType, Model, Page, PageContent, ProjectPage};
use crate::shared::styled_avatar::StyledAvatar;
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::StyledSelectChange;
use crate::shared::{drag_ev, inner_layout, ToNode};
use crate::{BoardPageChange, EditIssueModalSection, FieldId, Msg, PageChanged};
use crate::shared::{inner_layout, ToNode};
use crate::ws::send_ws_msg;
use crate::{BoardPageChange, EditIssueModalSection, FieldId, Msg, PageChanged, WebSocketChanged};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() {
@ -33,16 +33,16 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
};
match msg {
Msg::WsMsg(WsMsg::AuthorizeLoaded(..))
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
| Msg::ChangePage(Page::Project)
| Msg::ChangePage(Page::AddIssue)
| Msg::ChangePage(Page::EditIssue(..)) => {
send_ws_msg(jirs_data::WsMsg::ProjectRequest);
send_ws_msg(jirs_data::WsMsg::ProjectIssuesRequest);
send_ws_msg(jirs_data::WsMsg::ProjectUsersRequest);
send_ws_msg(jirs_data::WsMsg::IssueStatusesRequest);
send_ws_msg(jirs_data::WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(jirs_data::WsMsg::ProjectIssuesRequest, model.ws.as_ref());
send_ws_msg(jirs_data::WsMsg::ProjectUsersRequest, model.ws.as_ref());
send_ws_msg(jirs_data::WsMsg::IssueStatusesRequest, model.ws.as_ref());
}
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
let mut old: Vec<Issue> = vec![];
std::mem::swap(&mut old, &mut model.issues);
for is in old {
@ -53,7 +53,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
}
}
}
Msg::WsMsg(WsMsg::IssueDeleted(id)) => {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueDeleted(id))) => {
let mut old: Vec<Issue> = vec![];
std::mem::swap(&mut old, &mut model.issues);
for is in old {
@ -123,7 +123,10 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
project_page.issue_drag.clear_last();
}
Msg::DeleteIssue(issue_id) => {
send_ws_msg(jirs_data::WsMsg::IssueDeleteRequest(issue_id));
send_ws_msg(
jirs_data::WsMsg::IssueDeleteRequest(issue_id),
model.ws.as_ref(),
);
}
_ => (),
}
@ -306,16 +309,16 @@ fn project_issue_list(model: &Model, status: &jirs_data::IssueStatus) -> Node<Ms
let send_status = status.id;
let drop_handler = drag_ev(Ev::Drop, move |ev| {
ev.prevent_default();
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(
send_status,
Some(Msg::PageChanged(PageChanged::Board(
BoardPageChange::IssueDropZone(send_status),
)))
});
let send_status = status.id;
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
ev.prevent_default();
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragOverStatus(
send_status,
Some(Msg::PageChanged(PageChanged::Board(
BoardPageChange::IssueDragOverStatus(send_status),
)))
});
@ -395,25 +398,27 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
let issue_id = issue.id;
let drag_started = drag_ev(Ev::DragStart, move |_| {
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(
issue_id,
Some(Msg::PageChanged(PageChanged::Board(
BoardPageChange::IssueDragStarted(issue_id),
)))
});
let drag_stopped = drag_ev(Ev::DragEnd, move |_| {
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStopped(
issue_id,
Some(Msg::PageChanged(PageChanged::Board(
BoardPageChange::IssueDragStopped(issue_id),
)))
});
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
ev.prevent_default();
ev.stop_propagation();
Msg::PageChanged(PageChanged::Board(BoardPageChange::ExchangePosition(
issue_id,
Some(Msg::PageChanged(PageChanged::Board(
BoardPageChange::ExchangePosition(issue_id),
)))
});
let issue_id = issue.id;
let drag_out = drag_ev(Ev::DragLeave, move |_| {
Msg::PageChanged(PageChanged::Board(BoardPageChange::DragLeave(issue_id)))
Some(Msg::PageChanged(PageChanged::Board(
BoardPageChange::DragLeave(issue_id),
)))
});
let class_list = vec!["issue"];
@ -428,7 +433,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
drag_stopped,
drag_over_handler,
drag_out,
p![attrs![At::Class => "title"], issue.title,],
p![attrs![At::Class => "title"], issue.title.as_str()],
div![
attrs![At::Class => "bottom"],
div![

View File

@ -5,7 +5,6 @@ use jirs_data::{
IssueStatus, IssueStatusId, ProjectCategory, TimeTracking, ToVec, UpdateProjectPayload, WsMsg,
};
use crate::api::send_ws_msg;
use crate::model::{
DeleteIssueStatusModal, ModalType, Model, Page, PageContent, ProjectSettingsPage,
};
@ -18,9 +17,12 @@ use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::{drag_ev, inner_layout, ToChild, ToNode};
use crate::shared::{inner_layout, ToChild, ToNode};
use crate::ws::send_ws_msg;
use crate::FieldChange::TabChanged;
use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange};
use crate::{
model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange, WebSocketChanged,
};
static TIME_TRACKING_FIBONACCI: &'static str = "Tracking employees time carries the risk of having them feel like they are being spied on. This is one of the most common fears that employees have when a time tracking system is implemented. No one likes to feel like theyre always being watched.";
static TIME_TRACKING_HOURLY: &'static str = "Employees may feel intimidated by demands to track their time. Or they could feel that theyre constantly being watched and evaluated. And for overly ambitious managers, employee time tracking may open the doors to excessive micromanaging.";
@ -31,22 +33,25 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
}
match msg {
Msg::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
send_ws_msg(WsMsg::ProjectRequest);
send_ws_msg(WsMsg::IssueStatusesRequest);
send_ws_msg(WsMsg::ProjectIssuesRequest);
}
Msg::WebSocketChange(ref change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::IssueStatusesRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectIssuesRequest, model.ws.as_ref());
}
WebSocketChanged::WsMsg(WsMsg::ProjectLoaded(..)) => {
build_page_content(model);
}
_ => (),
},
Msg::ChangePage(Page::ProjectSettings) => {
build_page_content(model);
if model.user.is_some() {
send_ws_msg(WsMsg::ProjectRequest);
send_ws_msg(WsMsg::IssueStatusesRequest);
send_ws_msg(WsMsg::ProjectIssuesRequest);
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::IssueStatusesRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectIssuesRequest, model.ws.as_ref());
}
}
Msg::WsMsg(WsMsg::ProjectLoaded(..)) => {
build_page_content(model);
}
_ => (),
}
@ -86,14 +91,17 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
page.description_mode = mode;
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::SubmitForm)) => {
send_ws_msg(WsMsg::ProjectUpdateRequest(UpdateProjectPayload {
id: page.payload.id,
name: page.payload.name.clone(),
url: page.payload.url.clone(),
description: page.payload.description.clone(),
category: page.payload.category.clone(),
time_tracking: Some(page.time_tracking.value.into()),
}));
send_ws_msg(
WsMsg::ProjectUpdateRequest(UpdateProjectPayload {
id: page.payload.id,
name: page.payload.name.clone(),
url: page.payload.url.clone(),
description: page.payload.description.clone(),
category: page.payload.category.clone(),
time_tracking: Some(page.time_tracking.value.into()),
}),
model.ws.as_ref(),
);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStarted(
issue_status_id,
@ -128,7 +136,10 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
.find(|is| Some(is.id) == old_id)
.map(|is| (is.id, is.position))
{
send_ws_msg(WsMsg::IssueStatusUpdate(id, name.to_string(), pos))
send_ws_msg(
WsMsg::IssueStatusUpdate(id, name.to_string(), pos),
model.ws.as_ref(),
);
}
}
page.name.value = model
@ -259,91 +270,15 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let columns: Vec<Node<Msg>> = model
.issue_statuses
.iter()
.map(|is| {
let id = is.id;
let drag_started = drag_ev(Ev::DragStart, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStarted(
id,
)))
});
let drag_stopped = drag_ev(Ev::DragEnd, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStopped(
id,
)))
});
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
ev.prevent_default();
ev.stop_propagation();
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnExchangePosition(
id,
)))
});
let drag_out = drag_ev(Ev::DragLeave, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragLeave(id)))
});
if page.edit_column_id == Some(id) {
let blur = ev("focusout", |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(None)))
});
let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
.state(&page.name)
.primary()
.auto_focus()
.on_input_ev(blur)
.build()
.into_node();
div![
class!["columnPreview"],
div![class!["columnName"], input]
]
} else {
let on_edit = mouse_ev(Ev::Click, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(Some(id))))
});
let issue_count_in_column = per_column_issue_count.get(&id).cloned().unwrap_or_default();
let delete_row = if issue_count_in_column == 0 {
let on_delete = mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
ev.stop_propagation();
Msg::ModalOpened(Box::new(ModalType::DeleteIssueStatusModal(Box::new(DeleteIssueStatusModal::new(id)))))
});
let delete = StyledButton::build()
.primary()
.add_class("removeColumn")
.icon(Icon::Trash)
.on_click(on_delete)
.build()
.into_node();
div![class!["removeColumn"], delete]
} else {
div![class!["issueCount"], format!("Issues in column: {}", issue_count_in_column)]
};
div![
class!["columnPreview"],
attrs![At::Style => column_style.as_str(), At::Draggable => "true", At::DropZone => "true"],
div![class!["columnName"], span![is.name], on_edit, delete_row],
drag_started,
drag_stopped,
drag_over_handler,
drag_out,
]
}
})
.map(|is| column_preview(is, page, &per_column_issue_count, column_style.as_str()))
.collect();
let add_column = StyledIcon::build(Icon::Plus).build().into_node();
let columns_section = section![
class!["columnsSection"],
div![
class!["columns"],
columns,
div![
class!["columnPreview"],
attrs![At::Style => column_style.as_str()],
div![class!["columnName addColumn"], add_column]
]
add_column(page, column_style.as_str())
]
];
let columns_field = StyledField::build()
@ -451,7 +386,10 @@ fn sync(model: &mut Model) {
Some(is) => is,
_ => continue,
};
send_ws_msg(WsMsg::IssueStatusUpdate(*id, name.clone(), *position))
send_ws_msg(
WsMsg::IssueStatusUpdate(*id, name.clone(), *position),
model.ws.as_ref(),
);
}
}
@ -462,3 +400,137 @@ fn build_page_content(model: &mut Model) {
};
model.page_content = PageContent::ProjectSettings(Box::new(ProjectSettingsPage::new(project)));
}
fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
let on_click = mouse_ev(Ev::Click, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::EditIssueStatusName(Some(0)),
))
});
if page.edit_column_id == Some(0) {
let blur = ev("focusout", |_| {
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::EditIssueStatusName(None),
))
});
let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
.state(&page.name)
.primary()
.auto_focus()
.on_input_ev(blur)
.build()
.into_node();
div![class!["columnPreview"], div![class!["columnName"], input]]
} else {
let add_column = StyledIcon::build(Icon::Plus).build().into_node();
div![
class!["columnPreview"],
attrs![At::Style => column_style],
div![class!["columnName addColumn"], add_column],
on_click,
]
}
}
fn column_preview(
is: &IssueStatus,
page: &ProjectSettingsPage,
per_column_issue_count: &HashMap<i32, i32>,
column_style: &str,
) -> Node<Msg> {
if page.edit_column_id == Some(is.id) {
let blur = ev("focusout", |_| {
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::EditIssueStatusName(None),
))
});
let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
.state(&page.name)
.primary()
.auto_focus()
.on_input_ev(blur)
.build()
.into_node();
div![class!["columnPreview"], div![class!["columnName"], input]]
} else {
show_column_preview(is, per_column_issue_count, column_style)
}
}
fn show_column_preview(
is: &IssueStatus,
per_column_issue_count: &HashMap<i32, i32>,
column_style: &str,
) -> Node<Msg> {
let id = is.id;
let drag_started = drag_ev(Ev::DragStart, move |_| {
Some(Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::ColumnDragStarted(id),
)))
});
let drag_stopped = drag_ev(Ev::DragEnd, move |_| {
Some(Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::ColumnDragStopped(id),
)))
});
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
ev.prevent_default();
ev.stop_propagation();
Some(Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::ColumnExchangePosition(id),
)))
});
let drag_out = drag_ev(Ev::DragLeave, move |_| {
Some(Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::ColumnDragLeave(id),
)))
});
let on_edit = mouse_ev(Ev::Click, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::EditIssueStatusName(Some(id)),
))
});
let issue_count_in_column = per_column_issue_count.get(&id).cloned().unwrap_or_default();
let delete_row = if issue_count_in_column == 0 {
let on_delete = mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
ev.stop_propagation();
Msg::ModalOpened(Box::new(ModalType::DeleteIssueStatusModal(Box::new(
DeleteIssueStatusModal::new(id),
))))
});
let delete = StyledButton::build()
.primary()
.add_class("removeColumn")
.icon(Icon::Trash)
.on_click(on_delete)
.build()
.into_node();
div![class!["removeColumn"], delete]
} else {
div![
class!["issueCount"],
format!("Issues in column: {}", issue_count_in_column)
]
};
div![
class!["columnPreview"],
attrs![At::Style => column_style, At::Draggable => "true", At::DropZone => "true"],
div![
class!["columnName"],
span![is.name.as_str()],
on_edit,
delete_row
],
drag_started,
drag_stopped,
drag_over_handler,
drag_out,
]
}

View File

@ -15,7 +15,7 @@ pub fn render(model: &Model) -> Node<Msg> {
project_icon,
div![
class!["projectTexts"],
div![class!["projectName"], project.name],
div![class!["projectName"], project.name.as_str()],
div![class!["projectCategory"], project.category.to_string()]
],
],

View File

@ -1,5 +1,5 @@
use seed::{prelude::*, *};
use wasm_bindgen::JsCast;
use std::str::FromStr;
use jirs_data::*;
@ -33,6 +33,18 @@ pub trait ToChild {
fn to_child(&self) -> Self::Builder;
}
pub fn go_to_board() {
go_to("/board");
}
pub fn go_to_login() {
go_to("/login");
}
pub fn go_to(url: &str) {
seed::push_route(Url::from_str(url).unwrap());
}
pub fn find_issue(model: &Model, issue_id: IssueId) -> Option<&Issue> {
model.issues.iter().find(|issue| issue.id == issue_id)
}
@ -107,13 +119,3 @@ pub fn read_auth_token() -> Result<uuid::Uuid, String> {
.parse()
.map_err(|_| "Bad token format".to_string())
}
pub fn drag_ev<Ms>(
trigger: impl Into<Ev>,
handler: impl FnOnce(web_sys::DragEvent) -> Ms + 'static + Clone,
) -> EventHandler<Ms> {
let closure_handler = move |event: web_sys::Event| {
(handler.clone())(event.dyn_ref::<web_sys::DragEvent>().unwrap().clone())
};
EventHandler::new(trigger, closure_handler)
}

View File

@ -102,11 +102,10 @@ impl ToNode for ChildBuilder {
} = self;
let id = field_id.as_ref().map(|f| f.to_string()).unwrap_or_default();
let handler = if let Some(field_id) = field_id {
mouse_ev(Ev::Click, move |_| Msg::U32InputChanged(field_id, value))
} else {
ev(Ev::FullScreenError, move |_| Msg::NoOp)
};
let field_id_clone = field_id.clone();
let handler: EventHandler<Msg> = mouse_ev(Ev::Click, move |_| {
field_id_clone.map(|field_id| Msg::U32InputChanged(field_id, value))
});
class_list.push("styledCheckboxChild".to_string());
class_list.push(if selected { "selected" } else { "" }.to_string());

View File

@ -222,11 +222,11 @@ pub fn render(values: StyledInput) -> Node<Msg> {
}));
input_handlers.push(ev(Ev::KeyUp, move |event| {
event.stop_propagation();
Msg::NoOp
None as Option<Msg>
}));
input_handlers.push(ev(Ev::Click, move |event| {
event.stop_propagation();
Msg::NoOp
None as Option<Msg>
}));
div![

View File

@ -122,7 +122,7 @@ pub fn render(values: StyledModal) -> Node<Msg> {
let close_handler = mouse_ev(Ev::Click, |_| Msg::ModalDropped);
let body_handler = mouse_ev(Ev::Click, |ev| {
ev.stop_propagation();
Msg::NoOp
None as Option<Msg>
});
let clickable_class = format!("clickableOverlay {}", variant.to_class_name());

View File

@ -343,7 +343,7 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
attrs![At::Class => select_class.join(" "), At::Style => dropdown_style.as_str()],
keyboard_ev(Ev::KeyUp, |ev| {
ev.stop_propagation();
Msg::NoOp
None as Option<Msg>
}),
div![
attrs![At::Class => format!("valueContainer {}", variant)],

View File

@ -161,7 +161,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
let resize_handler = ev(Ev::KeyUp, move |event| {
event.stop_propagation();
if handler_disable_auto_resize {
return Msg::NoOp;
return None as Option<Msg>;
}
let target = event.target().unwrap();
@ -172,7 +172,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
textarea
.style()
.set_css_text(format!("height: {min_height}px", min_height = min_height).as_str());
Msg::NoOp
None as Option<Msg>
});
let handler_disable_auto_resize = disable_auto_resize;

View File

@ -5,7 +5,6 @@ use uuid::Uuid;
use jirs_data::WsMsg;
use crate::api::send_ws_msg;
use crate::model::{Page, PageContent, SignInPage};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
@ -15,7 +14,8 @@ use crate::shared::styled_input::StyledInput;
use crate::shared::styled_link::StyledLink;
use crate::shared::{outer_layout, write_auth_token, ToNode};
use crate::validations::{is_email, is_token};
use crate::{model, FieldId, Msg, SignInFieldId};
use crate::ws::send_ws_msg;
use crate::{model, FieldId, Msg, SignInFieldId, WebSocketChanged};
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::SignIn {
@ -49,10 +49,10 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
page.token_touched = true;
}
Msg::SignInRequest => {
send_ws_msg(WsMsg::AuthenticateRequest(
page.email.clone(),
page.username.clone(),
));
send_ws_msg(
WsMsg::AuthenticateRequest(page.email.clone(), page.username.clone()),
model.ws.as_ref(),
);
}
Msg::BindClientRequest => {
let bind_token: uuid::Uuid = match Uuid::from_str(page.token.as_str()) {
@ -62,21 +62,24 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
return;
}
};
send_ws_msg(WsMsg::BindTokenCheck(bind_token));
send_ws_msg(WsMsg::BindTokenCheck(bind_token), model.ws.as_ref());
}
Msg::WsMsg(WsMsg::AuthenticateSuccess) => {
page.login_success = true;
}
Msg::WsMsg(WsMsg::BindTokenOk(access_token)) => {
match write_auth_token(Some(access_token)) {
Ok(msg) => {
orders.skip().send_msg(msg);
}
Err(e) => {
error!(e);
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthenticateSuccess) => {
page.login_success = true;
}
WebSocketChanged::WsMsg(WsMsg::BindTokenOk(access_token)) => {
match write_auth_token(Some(access_token)) {
Ok(msg) => {
orders.skip().send_msg(msg);
}
Err(e) => {
error!(e);
}
}
}
}
_ => (),
},
_ => (),
};
}

View File

@ -2,7 +2,6 @@ use seed::{prelude::*, *};
use jirs_data::{SignUpFieldId, WsMsg};
use crate::api::send_ws_msg;
use crate::model::{Page, PageContent, SignUpPage};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
@ -12,7 +11,8 @@ use crate::shared::styled_input::StyledInput;
use crate::shared::styled_link::StyledLink;
use crate::shared::{outer_layout, ToNode};
use crate::validations::is_email;
use crate::{model, FieldId, Msg};
use crate::ws::send_ws_msg;
use crate::{model, FieldId, Msg, WebSocketChanged};
pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>) {
if model.page != Page::SignUp {
@ -42,17 +42,20 @@ pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>
page.email_touched = true;
}
Msg::SignUpRequest => {
send_ws_msg(WsMsg::SignUpRequest(
page.email.clone(),
page.username.clone(),
));
}
Msg::WsMsg(WsMsg::SignUpSuccess) => {
page.sign_up_success = true;
}
Msg::WsMsg(WsMsg::SignUpPairTaken) => {
page.error = "Pair you give is either taken or is not matching".to_string();
send_ws_msg(
WsMsg::SignUpRequest(page.email.clone(), page.username.clone()),
model.ws.as_ref(),
);
}
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WsMsg(WsMsg::SignUpSuccess) => {
page.sign_up_success = true;
}
WebSocketChanged::WsMsg(WsMsg::SignUpPairTaken) => {
page.error = "Pair you give is either taken or is not matching".to_string();
}
_ => (),
},
_ => (),
}
}
@ -126,7 +129,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let error_row = if page.error.is_empty() {
empty![]
} else {
div![class!["error"], p![page.error]]
div![class!["error"], p![page.error.as_str()]]
};
let sign_up_form = StyledForm::build()

View File

@ -2,7 +2,6 @@ use seed::{prelude::*, *};
use jirs_data::{InvitationState, ToVec, UserRole, UsersFieldId, WsMsg};
use crate::api::send_ws_msg;
use crate::model::*;
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
@ -11,7 +10,8 @@ use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::*;
use crate::shared::{inner_layout, ToChild, ToNode};
use crate::validations::is_email;
use crate::{FieldId, Msg, PageChanged, UsersPageChange};
use crate::ws::send_ws_msg;
use crate::{FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged};
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
if let Msg::ChangePage(Page::Users) = msg {
@ -27,18 +27,50 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
page.user_role_state.update(&msg, orders);
match msg {
Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(_))) | Msg::ChangePage(Page::Users)
if model.user.is_some() =>
{
send_ws_msg(WsMsg::InvitationListRequest);
send_ws_msg(WsMsg::InvitedUsersRequest);
}
Msg::WsMsg(WsMsg::InvitedUsersLoaded(users)) => {
page.invited_users = users;
}
Msg::WsMsg(WsMsg::InvitationListLoaded(invitations)) => {
page.invitations = invitations;
Msg::ChangePage(Page::Users) if model.user.is_some() => {
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref());
send_ws_msg(WsMsg::InvitedUsersRequest, model.ws.as_ref());
}
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(Ok(_))) if model.user.is_some() => {
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref());
send_ws_msg(WsMsg::InvitedUsersRequest, model.ws.as_ref());
}
WebSocketChanged::WsMsg(WsMsg::InvitedUsersLoaded(users)) => {
page.invited_users = users;
}
WebSocketChanged::WsMsg(WsMsg::InvitationListLoaded(invitations)) => {
page.invitations = invitations;
}
WebSocketChanged::WsMsg(WsMsg::InvitationRevokeSuccess(id)) => {
let mut old = vec![];
std::mem::swap(&mut page.invitations, &mut old);
for mut invitation in old {
if id == invitation.id {
invitation.state = InvitationState::Revoked;
}
page.invitations.push(invitation);
}
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref());
}
WebSocketChanged::WsMsg(WsMsg::InvitedUserRemoveSuccess(email)) => {
let mut old = vec![];
std::mem::swap(&mut page.invited_users, &mut old);
for user in old {
if user.email != email {
page.invited_users.push(user);
}
}
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendSuccess) => {
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref());
page.form_state = InvitationFormState::Succeed;
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendFailure) => {
page.form_state = InvitationFormState::Failed;
}
_ => (),
},
Msg::PageChanged(PageChanged::Users(UsersPageChange::ResetForm)) => {
page.name.clear();
page.name_touched = false;
@ -64,43 +96,22 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
Msg::InviteRequest => {
page.form_state = InvitationFormState::Sent;
send_ws_msg(WsMsg::InvitationSendRequest {
name: page.name.clone(),
email: page.email.clone(),
})
}
Msg::WsMsg(WsMsg::InvitationRevokeSuccess(id)) => {
let mut old = vec![];
std::mem::swap(&mut page.invitations, &mut old);
for mut invitation in old {
if id == invitation.id {
invitation.state = InvitationState::Revoked;
}
page.invitations.push(invitation);
}
send_ws_msg(WsMsg::InvitationListRequest);
send_ws_msg(
WsMsg::InvitationSendRequest {
name: page.name.clone(),
email: page.email.clone(),
},
model.ws.as_ref(),
);
}
Msg::InviteRevokeRequest(invitation_id) => {
send_ws_msg(WsMsg::InvitationRevokeRequest(invitation_id));
send_ws_msg(
WsMsg::InvitationRevokeRequest(invitation_id),
model.ws.as_ref(),
);
}
Msg::InvitedUserRemove(email) => {
send_ws_msg(WsMsg::InvitedUserRemoveRequest(email));
}
Msg::WsMsg(WsMsg::InvitedUserRemoveSuccess(email)) => {
let mut old = vec![];
std::mem::swap(&mut page.invited_users, &mut old);
for user in old {
if user.email != email {
page.invited_users.push(user);
}
}
}
Msg::WsMsg(WsMsg::InvitationSendSuccess) => {
send_ws_msg(WsMsg::InvitationListRequest);
page.form_state = InvitationFormState::Succeed;
}
Msg::WsMsg(WsMsg::InvitationSendFailure) => {
page.form_state = InvitationFormState::Failed;
send_ws_msg(WsMsg::InvitedUserRemoveRequest(email), model.ws.as_ref());
}
_ => (),
}
@ -210,8 +221,8 @@ pub fn view(model: &Model) -> Node<Msg> {
.into_node();
li![
class!["user"],
span![user.name],
span![user.email],
span![user.name.as_str()],
span![user.email.as_str()],
span![format!("{}", user.user_role)],
remove,
]
@ -238,8 +249,8 @@ pub fn view(model: &Model) -> Node<Msg> {
li![
class!["invitation"],
attrs![At::Class => format!("{}", invitation.state)],
span![invitation.name],
span![invitation.email],
span![invitation.name.as_str()],
span![invitation.email.as_str()],
span![format!("{}", invitation.state)],
revoke,
]

View File

@ -2,8 +2,8 @@ use seed::*;
use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::{Model, PageContent};
use crate::ws::send_ws_msg;
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
let project_page = match &mut model.page_content {
@ -90,16 +90,22 @@ pub fn sync(model: &mut Model) {
continue;
}
send_ws_msg(WsMsg::IssueUpdateRequest(
issue.id,
IssueFieldId::IssueStatusId,
PayloadVariant::I32(issue.issue_status_id),
));
send_ws_msg(WsMsg::IssueUpdateRequest(
issue.id,
IssueFieldId::ListPosition,
PayloadVariant::I32(issue.list_position),
));
send_ws_msg(
WsMsg::IssueUpdateRequest(
issue.id,
IssueFieldId::IssueStatusId,
PayloadVariant::I32(issue.issue_status_id),
),
model.ws.as_ref(),
);
send_ws_msg(
WsMsg::IssueUpdateRequest(
issue.id,
IssueFieldId::ListPosition,
PayloadVariant::I32(issue.list_position),
),
model.ws.as_ref(),
);
}
project_page.issue_drag.clear();
}

View File

@ -4,57 +4,75 @@ use jirs_data::WsMsg;
use crate::model::*;
use crate::shared::write_auth_token;
use crate::{Msg, APP};
use crate::{Msg, WebSocketChanged};
pub mod issue;
pub fn handle(msg: WsMsg) {
let app = match unsafe { APP.as_mut().unwrap() }.write() {
Ok(app) => app,
pub fn send_ws_msg(msg: WsMsg, ws: Option<&WebSocket>) {
let ws = match ws {
Some(ws) => ws,
_ => return,
};
match msg {
WsMsg::Ping | WsMsg::Pong => {}
_ => app.update(Msg::WsMsg(msg)),
}
let binary = bincode::serialize(&msg).unwrap();
ws.send_bytes(binary.as_slice())
.expect("Failed to send ws msg");
}
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
if model.host_url.is_empty() {
return;
}
let url = model.ws_url.as_str();
model.ws = WebSocket::builder(url, orders)
.on_message(|msg| Msg::WebSocketChange(WebSocketChanged::WebSocketMessage(msg)))
.on_open(|| Msg::WebSocketChange(WebSocketChanged::WebSocketOpened))
.on_close(|_| Msg::WebSocketChange(WebSocketChanged::WebSocketClosed))
.on_error(|| {})
.build_and_open()
.ok();
}
pub async fn read_incoming(msg: WebSocketMessage) -> Msg {
let bytes = msg.bytes().await.unwrap_or_default();
Msg::WebSocketChange(WebSocketChanged::WebSocketMessageLoaded(bytes))
}
pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
// auth
Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => {
WsMsg::AuthorizeLoaded(Ok(user)) => {
model.user = Some(user.clone());
}
Msg::WsMsg(WsMsg::AuthorizeExpired) => {
WsMsg::AuthorizeExpired => {
if let Ok(msg) = write_auth_token(None) {
orders.skip().send_msg(msg);
}
}
// project
Msg::WsMsg(WsMsg::ProjectLoaded(project)) => {
WsMsg::ProjectLoaded(project) => {
model.project = Some(project.clone());
}
// issues
Msg::WsMsg(WsMsg::ProjectIssuesLoaded(v)) => {
WsMsg::ProjectIssuesLoaded(v) => {
let mut v = v.clone();
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
model.issues = v;
}
// issue statuses
Msg::WsMsg(WsMsg::IssueStatusesResponse(v)) => {
WsMsg::IssueStatusesResponse(v) => {
model.issue_statuses = v.clone();
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
}
Msg::WsMsg(WsMsg::IssueStatusCreated(is)) => {
WsMsg::IssueStatusCreated(is) => {
model.issue_statuses.push(is.clone());
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
}
Msg::WsMsg(WsMsg::IssueStatusUpdated(changed)) => {
WsMsg::IssueStatusUpdated(changed) => {
let mut old = vec![];
std::mem::swap(&mut model.issue_statuses, &mut old);
for is in old {
@ -68,7 +86,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
}
Msg::WsMsg(WsMsg::IssueDeleted(id)) => {
WsMsg::IssueDeleted(id) => {
let mut old = vec![];
std::mem::swap(&mut model.issue_statuses, &mut old);
for is in old {
@ -82,11 +100,11 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
.sort_by(|a, b| a.position.cmp(&b.position));
}
// users
Msg::WsMsg(WsMsg::ProjectUsersLoaded(v)) => {
WsMsg::ProjectUsersLoaded(v) => {
model.users = v.clone();
}
// comments
Msg::WsMsg(WsMsg::IssueCommentsLoaded(comments)) => {
WsMsg::IssueCommentsLoaded(comments) => {
let issue_id = match model.modals.get(0) {
Some(ModalType::EditIssue(issue_id, _)) => *issue_id,
_ => return,
@ -98,7 +116,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
v.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
model.comments = v;
}
Msg::WsMsg(WsMsg::CommentDeleted(comment_id)) => {
WsMsg::CommentDeleted(comment_id) => {
let mut old = vec![];
std::mem::swap(&mut model.comments, &mut old);
for comment in old.into_iter() {
@ -107,7 +125,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
}
}
Msg::WsMsg(WsMsg::AvatarUrlChanged(user_id, avatar_url)) => {
WsMsg::AvatarUrlChanged(user_id, avatar_url) => {
for user in model.users.iter_mut() {
if user.id == *user_id {
user.avatar_url = Some(avatar_url.clone());

View File

@ -24,7 +24,7 @@ if (process.env.NODE_ENV === "production") {
execSync('cargo build --bin jirs-css', {
cwd: jirDir,
});
const css = spawn('./target/debug/jirs-css', [
spawn('./target/debug/jirs-css', [
'-W',
'-O',
'./jirs-client/dev/styles.css'
@ -70,7 +70,7 @@ module.exports = {
devServer: {
contentBase: path.join(__dirname, 'dev'),
historyApiFallback: true,
hot: true,
hot: false,
port: process.env.JIRS_CLIENT_PORT || 6000,
host: process.env.JIRS_CLIENT_BIND || '0.0.0.0',
allowedHosts: [

View File

@ -50,7 +50,6 @@ byteorder = "1.0"
chrono = { version = "0.4", features = [ "serde" ] }
libc = { version = "0.2.0" }
pq-sys = { version = ">=0.3.0, <0.5.0" }
quickcheck = { version = "0.4" }
serde_json = { version = ">=0.8.0, <2.0" }
toml = "0.5.6"
bincode = "1.2.1"

View File

@ -153,53 +153,6 @@ table! {
}
}
table! {
use diesel::sql_types::*;
use jirs_data::sql::*;
/// Representation of the `issue_statuses` table.
///
/// (Automatically generated by Diesel.)
issue_statuses (id) {
/// The `id` column of the `issue_statuses` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
id -> Int4,
/// The `name` column of the `issue_statuses` table.
///
/// Its SQL type is `Varchar`.
///
/// (Automatically generated by Diesel.)
name -> Varchar,
/// The `position` column of the `issue_statuses` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
position -> Int4,
/// The `project_id` column of the `issue_statuses` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
project_id -> Int4,
/// The `created_at` column of the `issue_statuses` table.
///
/// Its SQL type is `Timestamp`.
///
/// (Automatically generated by Diesel.)
created_at -> Timestamp,
/// The `updated_at` column of the `issue_statuses` table.
///
/// Its SQL type is `Timestamp`.
///
/// (Automatically generated by Diesel.)
updated_at -> Timestamp,
}
}
table! {
use diesel::sql_types::*;
use jirs_data::sql::*;
@ -301,6 +254,53 @@ table! {
}
}
table! {
use diesel::sql_types::*;
use jirs_data::sql::*;
/// Representation of the `issue_statuses` table.
///
/// (Automatically generated by Diesel.)
issue_statuses (id) {
/// The `id` column of the `issue_statuses` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
id -> Int4,
/// The `name` column of the `issue_statuses` table.
///
/// Its SQL type is `Varchar`.
///
/// (Automatically generated by Diesel.)
name -> Varchar,
/// The `position` column of the `issue_statuses` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
position -> Int4,
/// The `project_id` column of the `issue_statuses` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
project_id -> Int4,
/// The `created_at` column of the `issue_statuses` table.
///
/// Its SQL type is `Timestamp`.
///
/// (Automatically generated by Diesel.)
created_at -> Timestamp,
/// The `updated_at` column of the `issue_statuses` table.
///
/// Its SQL type is `Timestamp`.
///
/// (Automatically generated by Diesel.)
updated_at -> Timestamp,
}
}
table! {
use diesel::sql_types::*;
use jirs_data::sql::*;
@ -489,8 +489,8 @@ allow_tables_to_appear_in_same_query!(
comments,
invitations,
issue_assignees,
issue_statuses,
issues,
issue_statuses,
projects,
tokens,
users,