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

81
Cargo.lock generated
View File

@ -652,6 +652,17 @@ dependencies = [
"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]]
name = "autocfg"
version = "1.0.1"
@ -851,6 +862,22 @@ dependencies = [
"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]]
name = "const_fn"
version = "0.4.9"
@ -1738,18 +1765,6 @@ dependencies = [
"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]]
name = "mio-uds"
version = "0.6.8"
@ -1885,6 +1900,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "ouroboros"
version = "0.14.0"
@ -2872,6 +2896,12 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.86"
@ -2883,6 +2913,21 @@ dependencies = [
"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]]
name = "thiserror"
version = "1.0.30"
@ -3010,20 +3055,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092"
dependencies = [
"bytes 0.5.6",
"fnv",
"futures-core",
"iovec",
"lazy_static",
"libc",
"memchr",
"mio 0.6.23",
"mio-named-pipes",
"mio-uds",
"num_cpus",
"pin-project-lite 0.1.12",
"signal-hook-registry",
"slab",
"tokio-macros",
"winapi 0.3.9",
]
@ -3042,14 +3083,15 @@ dependencies = [
"parking_lot",
"pin-project-lite 0.2.8",
"signal-hook-registry",
"tokio-macros",
"winapi 0.3.9",
]
[[package]]
name = "tokio-macros"
version = "0.2.6"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a"
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [
"proc-macro2",
"quote",
@ -3375,6 +3417,7 @@ dependencies = [
"base64",
"bcrypt",
"chrono",
"clap",
"jemallocator",
"jsonwebtoken",
"mime_guess",
@ -3385,7 +3428,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"tokio 0.2.25",
"tokio 1.16.1",
"tracing",
"tracing-actix-web",
"tracing-subscriber",

View File

@ -26,10 +26,11 @@ bcrypt = "0.10.1"
actix-web-httpauth = "0.6.0-beta.7"
jsonwebtoken = "8.0.1"
rand = "0.8.4"
tokio = { verison = "1", features=["full"] }
tokio = { version = "1.16.1", features=["full"] }
base64 = "0.13.0"
sqlx = { version = "^0.5", features=["sqlite", "migrate"] }
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]

View File

@ -2,6 +2,7 @@ use std::{collections::HashMap, path::Path};
use actix::SystemService;
use actix_web::{get, web, App, HttpResponse, HttpServer};
use clap::crate_version;
use rust_embed::RustEmbed;
use sea_orm::{prelude::*, Database};
use tokio::sync::Mutex;
@ -27,6 +28,29 @@ pub struct AppState {
#[actix_rt::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(
tracing_subscriber::fmt::Layer::new()
.pretty()
@ -36,13 +60,18 @@ async fn main() {
);
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 {
db,
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();
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 || {
let cors = actix_cors::Cors::permissive();
App::new()
@ -89,7 +120,7 @@ async fn main() {
.service(api::routes())
.service(dist)
})
.bind("0.0.0.0:8088")
.bind(listen_host)
.unwrap()
.run()
.await
@ -118,10 +149,10 @@ async fn dist(path: web::Path<String>) -> HttpResponse {
}
#[instrument]
async fn setup_database() -> error::Result<DatabaseConnection> {
async fn setup_database(db_path: &str) -> error::Result<DatabaseConnection> {
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)?;
}

View File

@ -2,14 +2,14 @@
<modal :open="open">
<template #body>
<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" />
</form>
</template>
<template #actions>
<btn @click="delCategory" label="Delete" danger v-if="!!this.category.id" />
<btn @click="close" label="Cancel" />
<btn primary @click="submit" :label="saveLabel" />
<btn primary @click="submit" :label="saveLabel" :disabled="!allowSave"/>
</template>
</modal>
</template>
@ -39,6 +39,9 @@ export default {
saveLabel() {
return this.category.id ? "Update" : "Save";
},
allowSave() {
return !!this.category.categoryName;
}
},
methods: {
close() {

View File

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

View File

@ -2,14 +2,14 @@
<modal :open="open">
<template #body>
<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" />
</form>
</template>
<template #actions>
<btn @click="delCategory" label="Delete" danger v-if="!!this.category.id" />
<btn @click="close" label="Cancel" />
<btn primary @click="submit" :label="saveLabel" />
<btn primary :disabled="!allowSave" @click="submit" :label="saveLabel" />
</template>
</modal>
</template>
@ -39,6 +39,9 @@ export default {
saveLabel() {
return this.category.id ? "Update" : "Save";
},
allowSave() {
return this.category.categoryName
}
},
methods: {
close() {

View File

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

View File

@ -1,5 +1,5 @@
<template>
<button :class="{danger: danger, primary: primary}">{{label}}</button>
<button :disabled="disabled" :class="{danger: danger, primary: primary, disabled: disabled}">{{label}}</button>
</template>
<script>
@ -8,6 +8,7 @@ export default {
label: String,
danger: Boolean,
primary: Boolean,
disabled: Boolean,
},
};
</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;
}
button.danger {
background: #41141B;
button.danger {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>

View File

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

View File

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