Field Validation

Create and edit modal forms are now validated for required input.
This commit is contained in:
Joe Bellus 2022-02-12 17:04:32 -05:00
parent 18c5af9c43
commit 8a5ba5a0b1
11 changed files with 160 additions and 60 deletions

View File

@ -23,7 +23,7 @@ build-ui:
- npm install - npm install
- npm run build - npm run build
rules: rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
artifacts: artifacts:
paths: paths:
- dist/ - dist/
@ -36,7 +36,7 @@ build-x64-bin:
tags: tags:
- linux - linux
rules: rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script: script:
- cargo build --release - cargo build --release
- cd target/release - cd target/release
@ -54,7 +54,7 @@ build-musl-bin:
stage: build stage: build
image: 'rust:latest' image: 'rust:latest'
rules: rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script: script:
- rustup target add x86_64-unknown-linux-musl - rustup target add x86_64-unknown-linux-musl
- apt update && apt install -y musl-tools musl-dev - apt update && apt install -y musl-tools musl-dev
@ -74,7 +74,7 @@ build-arm-bin:
stage: build stage: build
image: 'rust:latest' image: 'rust:latest'
rules: rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script: script:
- rustup target add armv7-unknown-linux-gnueabihf - rustup target add armv7-unknown-linux-gnueabihf
- apt update - apt update
@ -113,7 +113,7 @@ deploy-dev-docker:
- docker build -t $CI_REGISTRY/vade/vade-mecum . - docker build -t $CI_REGISTRY/vade/vade-mecum .
- docker push $CI_REGISTRY/vade/vade-mecum - docker push $CI_REGISTRY/vade/vade-mecum
rules: rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
deploy-binaries: deploy-binaries:
dependencies: dependencies:

81
Cargo.lock generated
View File

@ -652,6 +652,17 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.0.1" version = "1.0.1"
@ -851,6 +862,22 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "clap"
version = "3.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62"
dependencies = [
"atty",
"bitflags",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
]
[[package]] [[package]]
name = "const_fn" name = "const_fn"
version = "0.4.9" version = "0.4.9"
@ -1738,18 +1765,6 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "mio-named-pipes"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656"
dependencies = [
"log",
"mio 0.6.23",
"miow 0.3.7",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "mio-uds" name = "mio-uds"
version = "0.6.8" version = "0.6.8"
@ -1885,6 +1900,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "ouroboros" name = "ouroboros"
version = "0.14.0" version = "0.14.0"
@ -2872,6 +2896,12 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.86" version = "1.0.86"
@ -2883,6 +2913,21 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.30" version = "1.0.30"
@ -3010,20 +3055,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092"
dependencies = [ dependencies = [
"bytes 0.5.6", "bytes 0.5.6",
"fnv",
"futures-core", "futures-core",
"iovec", "iovec",
"lazy_static", "lazy_static",
"libc", "libc",
"memchr", "memchr",
"mio 0.6.23", "mio 0.6.23",
"mio-named-pipes",
"mio-uds", "mio-uds",
"num_cpus",
"pin-project-lite 0.1.12", "pin-project-lite 0.1.12",
"signal-hook-registry", "signal-hook-registry",
"slab", "slab",
"tokio-macros",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -3042,14 +3083,15 @@ dependencies = [
"parking_lot", "parking_lot",
"pin-project-lite 0.2.8", "pin-project-lite 0.2.8",
"signal-hook-registry", "signal-hook-registry",
"tokio-macros",
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "0.2.6" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3375,6 +3417,7 @@ dependencies = [
"base64", "base64",
"bcrypt", "bcrypt",
"chrono", "chrono",
"clap",
"jemallocator", "jemallocator",
"jsonwebtoken", "jsonwebtoken",
"mime_guess", "mime_guess",
@ -3385,7 +3428,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"tokio 0.2.25", "tokio 1.16.1",
"tracing", "tracing",
"tracing-actix-web", "tracing-actix-web",
"tracing-subscriber", "tracing-subscriber",

View File

@ -26,10 +26,11 @@ bcrypt = "0.10.1"
actix-web-httpauth = "0.6.0-beta.7" actix-web-httpauth = "0.6.0-beta.7"
jsonwebtoken = "8.0.1" jsonwebtoken = "8.0.1"
rand = "0.8.4" rand = "0.8.4"
tokio = { verison = "1", features=["full"] } tokio = { version = "1.16.1", features=["full"] }
base64 = "0.13.0" base64 = "0.13.0"
sqlx = { version = "^0.5", features=["sqlite", "migrate"] } sqlx = { version = "^0.5", features=["sqlite", "migrate"] }
reqwest = { version = "0.11.9", features = ["rustls-tls"], default-features=false } reqwest = { version = "0.11.9", features = ["rustls-tls"], default-features=false }
clap = { version = "3.0.14", features=["cargo", "env"] }
[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator] [target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator]

View File

@ -2,6 +2,7 @@ use std::{collections::HashMap, path::Path};
use actix::SystemService; use actix::SystemService;
use actix_web::{get, web, App, HttpResponse, HttpServer}; use actix_web::{get, web, App, HttpResponse, HttpServer};
use clap::crate_version;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use sea_orm::{prelude::*, Database}; use sea_orm::{prelude::*, Database};
use tokio::sync::Mutex; use tokio::sync::Mutex;
@ -27,6 +28,29 @@ pub struct AppState {
#[actix_rt::main] #[actix_rt::main]
async fn main() { async fn main() {
let opts = clap::App::new("Vade Mecum")
.version(crate_version!())
.arg(
clap::Arg::new("port")
.short('p')
.env("VADE_PORT")
.long("port")
.value_name("number")
.default_value("8089")
.help("Set the port for the HTTP server")
.takes_value(true),
)
.arg(
clap::Arg::new("db")
.env("VADE_DB")
.long("db")
.value_name("path")
.default_value("./")
.help("Sets the path to the database location")
.takes_value(true),
)
.get_matches();
let subscriber = tracing_subscriber::registry().with( let subscriber = tracing_subscriber::registry().with(
tracing_subscriber::fmt::Layer::new() tracing_subscriber::fmt::Layer::new()
.pretty() .pretty()
@ -36,13 +60,18 @@ async fn main() {
); );
tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector"); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector");
let db = setup_database().await.unwrap(); let db = setup_database(opts.value_of("db").unwrap_or_default())
.await
.unwrap();
let state = web::Data::new(AppState { let state = web::Data::new(AppState {
db, db,
healthcheck_status: Mutex::new(HashMap::new()), healthcheck_status: Mutex::new(HashMap::new()),
}); });
info!("Starting http server on 8080"); info!(
"Starting http server on {}",
opts.value_of("port").unwrap_or_default()
);
let st = state.clone(); let st = state.clone();
actix_rt::spawn(async move { actix_rt::spawn(async move {
@ -79,6 +108,8 @@ async fn main() {
} }
}); });
let listen_host = format!("0.0.0.0:{}", opts.value_of("port").unwrap_or_default());
HttpServer::new(move || { HttpServer::new(move || {
let cors = actix_cors::Cors::permissive(); let cors = actix_cors::Cors::permissive();
App::new() App::new()
@ -89,7 +120,7 @@ async fn main() {
.service(api::routes()) .service(api::routes())
.service(dist) .service(dist)
}) })
.bind("0.0.0.0:8088") .bind(listen_host)
.unwrap() .unwrap()
.run() .run()
.await .await
@ -118,10 +149,10 @@ async fn dist(path: web::Path<String>) -> HttpResponse {
} }
#[instrument] #[instrument]
async fn setup_database() -> error::Result<DatabaseConnection> { async fn setup_database(db_path: &str) -> error::Result<DatabaseConnection> {
let db_fname = "data.db"; let db_fname = "data.db";
if !Path::new(db_fname).exists() { if !Path::new(db_path).join(db_fname).exists() {
std::fs::File::create(db_fname)?; std::fs::File::create(db_fname)?;
} }

View File

@ -2,14 +2,14 @@
<modal :open="open"> <modal :open="open">
<template #body> <template #body>
<form @submit.prevent="save"> <form @submit.prevent="save">
<text-field v-model="category.categoryName" label="Name" /> <text-field required v-model="category.categoryName" label="Name" />
<icon-picker v-model="category.glyph" /> <icon-picker v-model="category.glyph" />
</form> </form>
</template> </template>
<template #actions> <template #actions>
<btn @click="delCategory" label="Delete" danger v-if="!!this.category.id" /> <btn @click="delCategory" label="Delete" danger v-if="!!this.category.id" />
<btn @click="close" label="Cancel" /> <btn @click="close" label="Cancel" />
<btn primary @click="submit" :label="saveLabel" /> <btn primary @click="submit" :label="saveLabel" :disabled="!allowSave"/>
</template> </template>
</modal> </modal>
</template> </template>
@ -39,6 +39,9 @@ export default {
saveLabel() { saveLabel() {
return this.category.id ? "Update" : "Save"; return this.category.id ? "Update" : "Save";
}, },
allowSave() {
return !!this.category.categoryName;
}
}, },
methods: { methods: {
close() { close() {

View File

@ -9,9 +9,9 @@
:items="selectCategories" :items="selectCategories"
emptyLabel="No Category" emptyLabel="No Category"
/> />
<text-field v-model="app.appName" label="Name" /> <text-field v-model="app.appName" label="Name" required />
<text-field v-model="app.description" label="Description" /> <text-field v-model="app.description" label="Description" />
<text-field v-model="app.url" label="URL" /> <text-field v-model="app.url" label="URL" required />
<icon-picker v-model="app.glyph" /> <icon-picker v-model="app.glyph" />
<switch-field v-model="app.enableHealthcheck" label="Health check" /> <switch-field v-model="app.enableHealthcheck" label="Health check" />
</form> </form>
@ -19,7 +19,7 @@
<template #actions> <template #actions>
<btn @click="delApp" label="Delete" danger v-if="!!this.app.id" /> <btn @click="delApp" label="Delete" danger v-if="!!this.app.id" />
<btn @click="close" label="Cancel" /> <btn @click="close" label="Cancel" />
<btn primary :label="saveLabel" type="button" @click="save" /> <btn :disabled="!saveAllowed" primary :label="saveLabel" type="button" @click="save" />
</template> </template>
</modal> </modal>
</template> </template>
@ -54,6 +54,9 @@ export default {
selectCategories() { selectCategories() {
return this.categories.map((i) => ({ label: i.categoryName, value: i.id })); return this.categories.map((i) => ({ label: i.categoryName, value: i.id }));
}, },
saveAllowed() {
return (!!this.app.appName && !!this.app.url)
}
}, },
methods: { methods: {
close() { close() {

View File

@ -2,14 +2,14 @@
<modal :open="open"> <modal :open="open">
<template #body> <template #body>
<form @submit.prevent="save"> <form @submit.prevent="save">
<text-field v-model="category.categoryName" label="Name" /> <text-field required v-model="category.categoryName" label="Name" />
<icon-picker v-model="category.glyph" /> <icon-picker v-model="category.glyph" />
</form> </form>
</template> </template>
<template #actions> <template #actions>
<btn @click="delCategory" label="Delete" danger v-if="!!this.category.id" /> <btn @click="delCategory" label="Delete" danger v-if="!!this.category.id" />
<btn @click="close" label="Cancel" /> <btn @click="close" label="Cancel" />
<btn primary @click="submit" :label="saveLabel" /> <btn primary :disabled="!allowSave" @click="submit" :label="saveLabel" />
</template> </template>
</modal> </modal>
</template> </template>
@ -39,6 +39,9 @@ export default {
saveLabel() { saveLabel() {
return this.category.id ? "Update" : "Save"; return this.category.id ? "Update" : "Save";
}, },
allowSave() {
return this.category.categoryName
}
}, },
methods: { methods: {
close() { close() {

View File

@ -8,14 +8,14 @@
:items="selectCategories" :items="selectCategories"
emptyLabel="No Category" emptyLabel="No Category"
/> />
<text-field v-model="bookmark.bookmarkName" label="Name" /> <text-field required v-model="bookmark.bookmarkName" label="Name" />
<text-field v-model="bookmark.url" label="URL" /> <text-field required v-model="bookmark.url" label="URL" />
</form> </form>
</template> </template>
<template #actions> <template #actions>
<btn @click="delBookmark" label="Delete" danger v-if="!!this.bookmark.id" /> <btn @click="delBookmark" label="Delete" danger v-if="!!this.bookmark.id" />
<btn @click="close" label="Cancel" /> <btn @click="close" label="Cancel" />
<btn primary :label="saveLabel" type="button" @click="save" /> <btn primary :label="saveLabel" :disabled="!allowSave" type="button" @click="save" />
</template> </template>
</modal> </modal>
</template> </template>
@ -48,6 +48,9 @@ export default {
selectCategories() { selectCategories() {
return this.categories.map((i) => ({ label: i.categoryName, value: i.id })); return this.categories.map((i) => ({ label: i.categoryName, value: i.id }));
}, },
allowSave() {
return this.bookmark.bookmarkName && this.bookmark.url
}
}, },
methods: { methods: {
close() { close() {

View File

@ -1,5 +1,5 @@
<template> <template>
<button :class="{danger: danger, primary: primary}">{{label}}</button> <button :disabled="disabled" :class="{danger: danger, primary: primary, disabled: disabled}">{{label}}</button>
</template> </template>
<script> <script>
@ -8,6 +8,7 @@ export default {
label: String, label: String,
danger: Boolean, danger: Boolean,
primary: Boolean, primary: Boolean,
disabled: Boolean,
}, },
}; };
</script> </script>
@ -29,21 +30,18 @@ button:hover {
box-shadow: rgba(0, 0, 0, 0.42) 0px 1px 3px, rgba(0, 0, 0, 0.64) 0px 1px 2px; box-shadow: rgba(0, 0, 0, 0.42) 0px 1px 3px, rgba(0, 0, 0, 0.64) 0px 1px 2px;
} }
button.danger { button.danger {background: #41141B;}
background: #41141B; button.danger:hover {background: #61141B;}
button.primary {background: #21244B;}
button.primary:hover {background: #21246B;}
button.disabled, button.disabled:hover,
button.primary.disabled, button.primary.disabled:hover,
button.danger.disabled, button.danger.disabled:hover {
color: #777;
box-shadow: none;
background-color: transparent;
} }
button.danger:hover {
background: #61141B;
}
button.primary {
background: #21244B;
}
button.primary:hover {
background: #21246B;
}
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<form-field> <form-field>
<label v-if="label">{{label}}</label> <label v-if="label" :class="{error: isError}">{{label}}</label>
<input :value="modelValue" ref="field" @input="handleInput" :type="inputType" /> <input :value="modelValue" ref="field" @input="handleInput" :type="inputType" :class="{error: isError}" />
</form-field> </form-field>
</template> </template>
@ -9,6 +9,7 @@
import FormField from "./FormField.vue"; import FormField from "./FormField.vue";
export default { export default {
props: { props: {
required: Boolean,
label: String, label: String,
modelValue: String, modelValue: String,
password: Boolean, password: Boolean,
@ -17,8 +18,13 @@ export default {
inputType() { inputType() {
return this.password ? "password" : "text"; return this.password ? "password" : "text";
}, },
isError() {
return this.required && !this.modelValue
}
},
components: {
FormField
}, },
components: { FormField },
methods: { methods: {
handleInput() { handleInput() {
this.$emit("update:modelValue", this.$refs.field.value); this.$emit("update:modelValue", this.$refs.field.value);
@ -43,5 +49,14 @@ input {
input:focus { input:focus {
transition: all 0.2s; transition: all 0.2s;
border-bottom: 1px solid #336699; border-bottom: 1px solid #336699;
}
input.error {
border-bottom: 1px solid #900;
}
input.error:focus {
border-bottom: 1px solid #c00;
}
label.error {
color: #eee;
} }
</style> </style>

View File

@ -298,7 +298,7 @@ h2 svg {
cursor: pointer; cursor: pointer;
font-size: 44px; font-size: 44px;
font-weight: 900; font-weight: 900;
color: #252525; color: rgba(255,255,255,0.035);
text-shadow: 1px 1px rgba(0,0,0,0.15); text-shadow: 1px 1px rgba(0,0,0,0.15);
position: absolute; position: absolute;
bottom: 20px; bottom: 20px;