validation1 for the form
This commit is contained in:
@@ -8,7 +8,7 @@ license.workspace = true
|
||||
anyhow = { workspace = true }
|
||||
async-trait = "0.1.88"
|
||||
common = { path = "../common" }
|
||||
canvas = { path = "../canvas", features = ["gui", "suggestions", "cursor-style", "keymap"] }
|
||||
canvas = { path = "../canvas", features = ["gui", "suggestions", "cursor-style", "keymap", "validation"] }
|
||||
|
||||
ratatui = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
@@ -30,8 +30,9 @@ unicode-segmentation = "1.12.0"
|
||||
unicode-width.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["validation"]
|
||||
ui-debug = []
|
||||
validation = []
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.25.0"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// src/pages/forms/state.rs
|
||||
|
||||
use canvas::{DataProvider, AppMode};
|
||||
#[cfg(feature = "validation")]
|
||||
use canvas::{CharacterLimits, ValidationConfig, ValidationConfigBuilder};
|
||||
#[cfg(feature = "validation")]
|
||||
use canvas::validation::limits::CountMode;
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -39,6 +43,18 @@ pub struct FormState {
|
||||
pub autocomplete_loading: bool,
|
||||
pub link_display_map: HashMap<usize, String>,
|
||||
pub app_mode: AppMode,
|
||||
// Validation 1 (character limits) per field. None = no validation for that field.
|
||||
// Leave room for future rules (patterns, masks, etc.).
|
||||
pub char_limits: Vec<Option<CharLimitsRule>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "validation")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CharLimitsRule {
|
||||
pub min: Option<usize>,
|
||||
pub max: Option<usize>,
|
||||
pub warn_at: Option<usize>,
|
||||
pub count_mode: CountMode,
|
||||
}
|
||||
|
||||
impl FormState {
|
||||
@@ -56,6 +72,7 @@ impl FormState {
|
||||
fields: Vec<FieldDefinition>,
|
||||
) -> Self {
|
||||
let values = vec![String::new(); fields.len()];
|
||||
let len = values.len();
|
||||
FormState {
|
||||
id: 0,
|
||||
profile_name,
|
||||
@@ -73,6 +90,7 @@ impl FormState {
|
||||
autocomplete_loading: false,
|
||||
link_display_map: HashMap::new(),
|
||||
app_mode: canvas::AppMode::Edit,
|
||||
char_limits: vec![None; len],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +274,24 @@ impl FormState {
|
||||
pub fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
self.current_cursor_pos = pos;
|
||||
}
|
||||
|
||||
#[cfg(feature = "validation")]
|
||||
pub fn set_character_limits_rules(
|
||||
&mut self,
|
||||
rules: Vec<Option<CharLimitsRule>>,
|
||||
) {
|
||||
if rules.len() == self.fields.len() {
|
||||
self.char_limits = rules;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Character limits count {} != field count {} for {}.{}",
|
||||
rules.len(),
|
||||
self.fields.len(),
|
||||
self.profile_name,
|
||||
self.table_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Implement DataProvider for FormState
|
||||
@@ -282,4 +318,26 @@ impl DataProvider for FormState {
|
||||
fn supports_suggestions(&self, field_index: usize) -> bool {
|
||||
self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false)
|
||||
}
|
||||
|
||||
// Validation 1: Provide character-limit-based validation to canvas
|
||||
// Only compiled when the "validation" feature is enabled on canvas.
|
||||
#[cfg(feature = "validation")]
|
||||
fn validation_config(&self, index: usize) -> Option<ValidationConfig> {
|
||||
let rule = self.char_limits.get(index)?.as_ref()?;
|
||||
let mut limits = match (rule.min, rule.max) {
|
||||
(Some(min), Some(max)) => CharacterLimits::new_range(min, max),
|
||||
(None, Some(max)) => CharacterLimits::new(max),
|
||||
(Some(min), None) => CharacterLimits::new_range(min, usize::MAX),
|
||||
(None, None) => CharacterLimits::new(usize::MAX),
|
||||
};
|
||||
limits = limits.with_count_mode(rule.count_mode);
|
||||
if let Some(warn) = rule.warn_at {
|
||||
limits = limits.with_warning_threshold(warn);
|
||||
}
|
||||
Some(
|
||||
ValidationConfigBuilder::new()
|
||||
.with_character_limits(limits)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,14 @@ use common::proto::komp_ac::tables_data::{
|
||||
};
|
||||
use crate::search::SearchGrpc;
|
||||
use common::proto::komp_ac::search::SearchResponse;
|
||||
use common::proto::komp_ac::table_validation::{
|
||||
table_validation_service_client::TableValidationServiceClient,
|
||||
GetTableValidationRequest,
|
||||
TableValidationResponse,
|
||||
CountMode as PbCountMode,
|
||||
FieldValidation as PbFieldValidation,
|
||||
CharacterLimits as PbCharacterLimits,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use tonic::transport::{Channel, Endpoint};
|
||||
@@ -40,6 +48,7 @@ pub struct GrpcClient {
|
||||
table_script_client: TableScriptClient<Channel>,
|
||||
tables_data_client: TablesDataClient<Channel>,
|
||||
search_client: SearchGrpc,
|
||||
table_validation_client: TableValidationServiceClient<Channel>,
|
||||
}
|
||||
|
||||
impl GrpcClient {
|
||||
@@ -63,6 +72,8 @@ impl GrpcClient {
|
||||
let table_script_client = TableScriptClient::new(channel.clone());
|
||||
let tables_data_client = TablesDataClient::new(channel.clone());
|
||||
let search_client = SearchGrpc::new(channel.clone());
|
||||
let table_validation_client =
|
||||
TableValidationServiceClient::new(channel.clone());
|
||||
|
||||
Ok(Self {
|
||||
channel,
|
||||
@@ -71,6 +82,7 @@ impl GrpcClient {
|
||||
table_script_client,
|
||||
tables_data_client,
|
||||
search_client,
|
||||
table_validation_client,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,6 +91,24 @@ impl GrpcClient {
|
||||
self.channel.clone()
|
||||
}
|
||||
|
||||
// Fetch validation rules for a table. Absence of a field in response = no validation.
|
||||
pub async fn get_table_validation(
|
||||
&mut self,
|
||||
profile_name: String,
|
||||
table_name: String,
|
||||
) -> Result<TableValidationResponse> {
|
||||
let req = GetTableValidationRequest {
|
||||
profile_name,
|
||||
table_name,
|
||||
};
|
||||
let resp = self
|
||||
.table_validation_client
|
||||
.get_table_validation(tonic::Request::new(req))
|
||||
.await
|
||||
.context("gRPC GetTableValidation call failed")?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn get_table_structure(
|
||||
&mut self,
|
||||
profile_name: String,
|
||||
|
||||
@@ -6,6 +6,8 @@ use crate::pages::admin_panel::add_logic::state::AddLogicState;
|
||||
use crate::pages::forms::logic::SaveOutcome;
|
||||
use crate::utils::columns::filter_user_columns;
|
||||
use crate::pages::forms::{FieldDefinition, FormState};
|
||||
use common::proto::komp_ac::table_validation::CountMode as PbCountMode;
|
||||
use canvas::validation::limits::CountMode;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -314,4 +316,60 @@ impl UiService {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch and apply "Validation 1" (character limits) rules for this form.
|
||||
pub async fn apply_validation1_for_form(
|
||||
grpc_client: &mut GrpcClient,
|
||||
app_state: &mut AppState,
|
||||
path: &str,
|
||||
) -> Result<()> {
|
||||
let (profile, table) = path
|
||||
.split_once('/')
|
||||
.context("Invalid form path for validation")?;
|
||||
|
||||
let resp = grpc_client
|
||||
.get_table_validation(profile.to_string(), table.to_string())
|
||||
.await
|
||||
.context("Failed to fetch table validation")?;
|
||||
|
||||
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||
let mut rules: Vec<Option<crate::pages::forms::state::CharLimitsRule>> =
|
||||
vec![None; fs.fields.len()];
|
||||
|
||||
for f in resp.fields {
|
||||
if let Some(idx) = fs.fields.iter().position(|fd| fd.data_key == f.data_key) {
|
||||
if let Some(limits) = f.limits {
|
||||
let has_any =
|
||||
limits.min != 0 || limits.max != 0 || limits.warn_at.is_some();
|
||||
if has_any {
|
||||
let cm = match PbCountMode::from_i32(limits.count_mode) {
|
||||
Some(PbCountMode::Unspecified) | None => CountMode::Characters, // protobuf default → fallback
|
||||
Some(PbCountMode::Chars) => CountMode::Characters,
|
||||
Some(PbCountMode::Bytes) => CountMode::Bytes,
|
||||
Some(PbCountMode::DisplayWidth) => CountMode::DisplayWidth,
|
||||
};
|
||||
|
||||
let min = if limits.min == 0 { None } else { Some(limits.min as usize) };
|
||||
let max = if limits.max == 0 { None } else { Some(limits.max as usize) };
|
||||
let warn_at = limits.warn_at.map(|w| w as usize);
|
||||
|
||||
rules[idx] = Some(crate::pages::forms::state::CharLimitsRule {
|
||||
min,
|
||||
max,
|
||||
warn_at,
|
||||
count_mode: cm,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.set_character_limits_rules(rules);
|
||||
}
|
||||
|
||||
if let Some(editor) = app_state.editor_for_path(path) {
|
||||
editor.set_validation_enabled(true);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,10 @@ pub async fn run_ui() -> Result<()> {
|
||||
app_state.ensure_form_editor(&path, &config, || {
|
||||
FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs)
|
||||
});
|
||||
#[cfg(feature = "validation")]
|
||||
UiService::apply_validation1_for_form(&mut grpc_client, &mut app_state, &path)
|
||||
.await
|
||||
.ok();
|
||||
buffer_state.update_history(AppView::Form(path.clone()));
|
||||
router.navigate(Page::Form(path.clone()));
|
||||
|
||||
@@ -516,6 +520,21 @@ pub async fn run_ui() -> Result<()> {
|
||||
prev_view_profile_name = current_view_profile;
|
||||
prev_view_table_name = current_view_table;
|
||||
table_just_switched = true;
|
||||
// Apply character-limit validation for the new form
|
||||
#[cfg(feature = "validation")]
|
||||
if let (Some(prof), Some(tbl)) = (
|
||||
app_state.current_view_profile_name.as_ref(),
|
||||
app_state.current_view_table_name.as_ref(),
|
||||
) {
|
||||
let p = format!("{}/{}", prof, tbl);
|
||||
UiService::apply_validation1_for_form(
|
||||
&mut grpc_client,
|
||||
&mut app_state,
|
||||
&p,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
app_state.update_dialog_content(
|
||||
|
||||
Reference in New Issue
Block a user