flowbroker wip
This commit is contained in:
29
flowbroker/Cargo.lock
generated
29
flowbroker/Cargo.lock
generated
@@ -972,6 +972,12 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.1"
|
||||
@@ -994,6 +1000,8 @@ dependencies = [
|
||||
"rhai_wrapper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tst",
|
||||
]
|
||||
|
||||
@@ -1750,7 +1758,7 @@ dependencies = [
|
||||
name = "rhai_autobind_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -2045,6 +2053,25 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
|
@@ -12,7 +12,6 @@ use sigsocket::registry::ConnectionRegistry;
|
||||
use log::{info, error};
|
||||
use uuid::Uuid;
|
||||
use rhai::{Engine, EvalAltResult, Position};
|
||||
use serde_json::Value as JsonValue;
|
||||
// use std::collections::HashMap; // Removed as no longer used
|
||||
use heromodels; // Added for database models
|
||||
use heromodels::db::hero::OurDB;
|
||||
@@ -138,80 +137,70 @@ pub struct RhaiScriptFormData {
|
||||
}
|
||||
|
||||
|
||||
// --- Handlers ---
|
||||
|
||||
// Display list of flows
|
||||
async fn list_flows(data: web::Data<AppState>) -> ActixResult<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
match data.db.collection::<Flow>() {
|
||||
Ok(flow_collection) => {
|
||||
match flow_collection.get_all() {
|
||||
Ok(mut flows_vec) => {
|
||||
// Sort by creation date, newest first
|
||||
flows_vec.sort_by(|a, b| b.base_data.created_at.cmp(&a.base_data.created_at));
|
||||
context.insert("flows", &flows_vec);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to retrieve flows from database: {:?}", e);
|
||||
// Optionally, insert an empty vec or an error message for the template
|
||||
context.insert("flows", &Vec::<Flow>::new());
|
||||
context.insert("db_error", "Failed to load flows.");
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to get flow collection from database: {}", e);
|
||||
context.insert("flows", &Vec::<Flow>::new());
|
||||
context.insert("db_error", "Database collection error.");
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = data.templates.render("index.html", &context)
|
||||
.map_err(|e| {
|
||||
error!("Template error (index.html): {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template error rendering index.html")
|
||||
})?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
// Show form to create a new flow
|
||||
#[derive(Serialize, Clone)] // Clone is for the context, Serialize for Tera
|
||||
struct RhaiExampleScript {
|
||||
// --- Context Structs for Templates ---
|
||||
#[derive(Serialize, Clone)]
|
||||
struct RhaiExampleDisplay {
|
||||
name: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
async fn new_flow_form(data: web::Data<AppState>) -> impl Responder {
|
||||
let mut context = Context::new();
|
||||
let mut example_scripts = Vec::new();
|
||||
let examples_path = PathBuf::from("templates/rhai_examples");
|
||||
#[derive(Serialize)]
|
||||
struct ListFlowsContext {
|
||||
flows: Vec<Flow>, // Using heromodels::models::flowbroker_models::Flow
|
||||
example_scripts: Vec<RhaiExampleDisplay>,
|
||||
error_message: Option<String>,
|
||||
success_message: Option<String>,
|
||||
}
|
||||
|
||||
// --- Handlers ---
|
||||
|
||||
// Display list of flows
|
||||
async fn list_flows(data: web::Data<AppState>) -> ActixResult<HttpResponse> {
|
||||
let tera = &data.templates;
|
||||
|
||||
// Fetch actual flows from the database
|
||||
let flows_collection = data.db.collection::<Flow>()
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("DB Error: Failed to get flows collection: {}", e)))?;
|
||||
let (mut flows, flow_error_message) = match flows_collection.get_all() {
|
||||
Ok(mut flows_vec) => {
|
||||
flows_vec.sort_by(|a, b| b.base_data.created_at.cmp(&a.base_data.created_at)); // Sort by newest
|
||||
(flows_vec, None)
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to fetch flows: {:?}", e);
|
||||
(Vec::new(), Some(format!("Error fetching flows: {:?}", e)))
|
||||
}
|
||||
};
|
||||
|
||||
// Load Rhai example scripts
|
||||
let examples_path = PathBuf::from("templates/rhai_examples");
|
||||
let mut example_scripts_display = Vec::new();
|
||||
if examples_path.is_dir() {
|
||||
match std_fs::read_dir(examples_path) {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rhai") {
|
||||
if path.is_file() && path.extension().and_then(std::ffi::OsStr::to_str) == Some("rhai") {
|
||||
let file_stem = path.file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Unknown Script");
|
||||
// Convert filename (e.g., simple_two_step) to a nicer name (e.g., Simple Two Step)
|
||||
let script_name = file_stem.replace("_", " ")
|
||||
.split_whitespace()
|
||||
.map(|word| {
|
||||
let mut c = word.chars();
|
||||
match c.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>().join(" ");
|
||||
|
||||
match std_fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("Unknown Script");
|
||||
// Convert filename (e.g., simple_two_step) to a nicer name (e.g., Simple Two Step)
|
||||
let name = file_stem.replace("_", " ")
|
||||
.split_whitespace()
|
||||
.map(|word| {
|
||||
let mut c = word.chars();
|
||||
match c.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>().join(" ");
|
||||
example_scripts.push(RhaiExampleScript { name, content });
|
||||
example_scripts_display.push(RhaiExampleDisplay { name: script_name, content });
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to read Rhai example script {}: {}", path.display(), e);
|
||||
error!("Failed to read Rhai example script {:?}: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,20 +208,31 @@ async fn new_flow_form(data: web::Data<AppState>) -> impl Responder {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to read rhai_examples directory: {}", e);
|
||||
error!("Failed to read Rhai examples directory: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
example_scripts_display.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
context.insert("example_scripts", &example_scripts);
|
||||
info!("Rendering new flow form with {} examples from files.", example_scripts.len());
|
||||
match data.templates.render("new_flow_form.html", &context) {
|
||||
Ok(rendered) => HttpResponse::Ok().body(rendered),
|
||||
Err(e) => {
|
||||
error!("Template error in new_flow_form: {}", e);
|
||||
HttpResponse::InternalServerError().body(format!("Template error: {}", e))
|
||||
}
|
||||
}
|
||||
let list_context = ListFlowsContext {
|
||||
flows,
|
||||
example_scripts: example_scripts_display,
|
||||
error_message: flow_error_message,
|
||||
success_message: None, // TODO: Populate from query params or session later if needed
|
||||
};
|
||||
|
||||
let tera_ctx = Context::from_serialize(&list_context).unwrap_or_else(|e| {
|
||||
error!("Failed to serialize ListFlowsContext: {}", e);
|
||||
// Fallback to a minimal context or an error state if serialization fails
|
||||
let mut err_ctx = Context::new();
|
||||
err_ctx.insert("error_message", &"Critical error preparing page data.".to_string());
|
||||
err_ctx
|
||||
});
|
||||
|
||||
// Still rendering to index.html, which will be the revamped list_flows.html
|
||||
let rendered = tera.render("index.html", &tera_ctx)
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("Template error (index.html): {}", e)))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
// Handle creation of a new flow
|
||||
@@ -646,7 +646,7 @@ pub fn configure_app_routes(cfg: &mut web::ServiceConfig) {
|
||||
.service(
|
||||
web::scope("/flows") // Group flow-related routes under /flows
|
||||
// .route("", web::get().to(list_flows)) // If you want /flows to also list flows
|
||||
.route("/new", web::get().to(new_flow_form))
|
||||
// .route("/new", web::get().to(new_flow_form)) // Deprecated, functionality merged into root list_flows
|
||||
.route("/create", web::post().to(create_flow))
|
||||
.route("/create_script", web::post().to(create_flow_from_script)) // Moved inside /flows scope
|
||||
)
|
||||
|
@@ -2,27 +2,316 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Flowbroker - Flows</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FlowBroker Dashboard</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<!-- Optional: Link to your custom style.css if needed, but ensure it doesn't conflict heavily with Bootstrap -->
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Active Flows</h1>
|
||||
<a href="/flows/new">Create New Flow</a>
|
||||
<div id="flows-list">
|
||||
{% if flows %}
|
||||
<ul>
|
||||
{% for flow in flows %}
|
||||
<li>
|
||||
<strong>{{ flow.name }}</strong> (UUID: {{ flow.flow_uuid }}) - Status: {{ flow.status }}
|
||||
<br>
|
||||
Created: {{ flow.base_data.created_at | date(format="%Y-%m-%d %H:%M:%S") }} <!-- Assuming created_at is a Unix timestamp -->
|
||||
<p><a href="/flows/{{ flow.flow_uuid }}">View Details</a></p> <!-- Link uses flow_uuid -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">FlowBroker</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/">Dashboard</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No active flows. <a href="/flows/new">Create one?</a></p>
|
||||
<!-- Add other nav items here later if needed -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
{% if error_message %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if success_message %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ success_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2>Active Flows</h2>
|
||||
<div id="flows-list" class="mb-4">
|
||||
{% if flows %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">UUID</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Created At</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for flow in flows %}
|
||||
<tr>
|
||||
<td>{{ flow.name }}</td>
|
||||
<td><small>{{ flow.flow_uuid }}</small></td>
|
||||
<td><span class="badge bg-secondary">{{ flow.status | default(value="Unknown") }}</span></td>
|
||||
<td>{{ flow.base_data.created_at | date(format="%Y-%m-%d %H:%M:%S") }}</td>
|
||||
<td>
|
||||
<a href="/flows/{{ flow.flow_uuid }}" class="btn btn-sm btn-primary">View</a>
|
||||
<button class="btn btn-sm btn-success run-flow-btn" data-flow-uuid="{{ flow.flow_uuid }}">Run</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No active flows found. You can create one below.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h2>Runnable Example Scripts</h2>
|
||||
<div id="example-scripts-list" class="mb-4">
|
||||
{% if example_scripts %}
|
||||
<ul class="list-group">
|
||||
{% for example in example_scripts %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
{{ example.name }}
|
||||
<button class="btn btn-sm btn-info load-script-btn" data-script-content="{{ example.content | escape }}">Load into Form</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No example scripts found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<h2>Create New Flow from Rhai Script</h2>
|
||||
<div id="create-from-rhai-section">
|
||||
<form action="/flows/create_script" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="rhai_script_content" class="form-label">Rhai Script:</label>
|
||||
<textarea class="form-control" id="rhai_script_content" name="rhai_script" rows="10" placeholder="Enter your Rhai script here or select an example using the 'Load into Form' buttons above..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create Flow from Script</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-4">
|
||||
<h2>Create New Flow (Step-by-Step UI)</h2>
|
||||
<div id="create-step-by-step-section">
|
||||
<form id="createFlowForm_dynamic" action="/flows/create" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="flow_name_dynamic" class="form-label">Flow Name:</label>
|
||||
<input type="text" id="flow_name_dynamic" name="flow_name" class="form-control" required>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div id="stepsContainer_dynamic" class="mb-3">
|
||||
<!-- Steps will be added here by JavaScript -->
|
||||
<p class="text-muted"><em>Steps will appear here as you add them.</em></p>
|
||||
</div>
|
||||
|
||||
<button type="button" id="addStepBtn_dynamic" class="btn btn-secondary mb-3">Add Step</button>
|
||||
<hr>
|
||||
<button type="submit" class="btn btn-primary">Create Flow (Step-by-Step)</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Templates for Dynamic Step-by-Step Form -->
|
||||
<template id="stepTemplate_dynamic">
|
||||
<div class="step card mb-3" data-step-index="">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Step <span class="step-number"></span></h5>
|
||||
<button type="button" class="btn btn-danger btn-sm removeStepBtn_dynamic">Remove This Step</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Step Description (Optional):</label>
|
||||
<input type="text" name="steps[X].description" class="form-control step-description_dynamic">
|
||||
</div>
|
||||
<h6>Signature Requirements for Step <span class="step-number"></span>:</h6>
|
||||
<div class="requirementsContainer_dynamic ps-3" data-step-index="">
|
||||
<!-- Requirements will be added here by JS -->
|
||||
<p class="text-muted small"><em>Requirements for this step will appear here.</em></p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm addRequirementBtn_dynamic mt-2" data-step-index="">Add Signature Requirement</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="requirementTemplate_dynamic">
|
||||
<div class="requirement card mb-2" data-req-index="">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong>Requirement <span class="req-number"></span></strong>
|
||||
<button type="button" class="btn btn-danger btn-sm removeRequirementBtn_dynamic">Remove Requirement</button>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Message to Sign:</label>
|
||||
<textarea name="steps[X].requirements[Y].message" rows="2" class="form-control req-message_dynamic" required></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Required Public Key:</label>
|
||||
<input type="text" name="steps[X].requirements[Y].public_key" class="form-control req-pubkey_dynamic" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- End of Templates -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
<script>
|
||||
// Basic script to handle 'Load into Form' for example scripts
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const loadScriptButtons = document.querySelectorAll('.load-script-btn');
|
||||
loadScriptButtons.forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const scriptContent = this.dataset.scriptContent;
|
||||
const rhaiTextarea = document.querySelector('#rhai_script_content'); // Assuming this ID for the textarea
|
||||
if (rhaiTextarea) {
|
||||
rhaiTextarea.value = scriptContent;
|
||||
// Optionally, scroll to the form or give some visual feedback
|
||||
rhaiTextarea.focus();
|
||||
alert('Script loaded into the textarea below!');
|
||||
} else {
|
||||
alert('Rhai script textarea not found on the page. It will be added soon.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Start of Dynamic Step-by-Step Form Logic ---
|
||||
const stepsContainer_dynamic = document.getElementById('stepsContainer_dynamic');
|
||||
const addStepBtn_dynamic = document.getElementById('addStepBtn_dynamic');
|
||||
const stepTemplate_dynamic = document.getElementById('stepTemplate_dynamic');
|
||||
const requirementTemplate_dynamic = document.getElementById('requirementTemplate_dynamic');
|
||||
const form_dynamic = document.getElementById('createFlowForm_dynamic');
|
||||
|
||||
if (stepsContainer_dynamic && addStepBtn_dynamic && stepTemplate_dynamic && requirementTemplate_dynamic && form_dynamic) { // Check if all elements exist
|
||||
|
||||
const updateIndices_dynamic = () => {
|
||||
const steps = stepsContainer_dynamic.querySelectorAll('.step'); // .step is the class on the root of the cloned template
|
||||
steps.forEach((step, stepIdx) => {
|
||||
step.dataset.stepIndex = stepIdx;
|
||||
step.querySelector('.step-number').textContent = stepIdx + 1;
|
||||
const descriptionInput = step.querySelector('.step-description_dynamic');
|
||||
if(descriptionInput) descriptionInput.name = `steps[${stepIdx}].description`;
|
||||
|
||||
const addReqBtn = step.querySelector('.addRequirementBtn_dynamic');
|
||||
if (addReqBtn) addReqBtn.dataset.stepIndex = stepIdx;
|
||||
|
||||
const requirements = step.querySelectorAll('.requirementsContainer_dynamic .requirement'); // .requirement is class on root of its template
|
||||
requirements.forEach((req, reqIdx) => {
|
||||
req.dataset.reqIndex = reqIdx;
|
||||
req.querySelector('.req-number').textContent = reqIdx + 1;
|
||||
const messageTextarea = req.querySelector('.req-message_dynamic');
|
||||
if(messageTextarea) messageTextarea.name = `steps[${stepIdx}].requirements[${reqIdx}].message`;
|
||||
const pubkeyInput = req.querySelector('.req-pubkey_dynamic');
|
||||
if(pubkeyInput) pubkeyInput.name = `steps[${stepIdx}].requirements[${reqIdx}].public_key`;
|
||||
});
|
||||
});
|
||||
// Remove the initial placeholder message if steps are present
|
||||
const placeholder = stepsContainer_dynamic.querySelector('p.text-muted');
|
||||
if (steps.length > 0 && placeholder) {
|
||||
placeholder.style.display = 'none';
|
||||
} else if (steps.length === 0 && placeholder) {
|
||||
placeholder.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
const addRequirement_dynamic = (currentStepElement, stepIndex) => {
|
||||
const requirementsContainer = currentStepElement.querySelector('.requirementsContainer_dynamic');
|
||||
if (!requirementsContainer) return;
|
||||
const reqFragment = requirementTemplate_dynamic.content.cloneNode(true);
|
||||
const newRequirement = reqFragment.querySelector('.requirement'); // .requirement is class on root
|
||||
// Remove placeholder from requirements container if it exists
|
||||
const reqPlaceholder = requirementsContainer.querySelector('p.text-muted.small');
|
||||
if (reqPlaceholder) reqPlaceholder.style.display = 'none';
|
||||
|
||||
requirementsContainer.appendChild(newRequirement);
|
||||
updateIndices_dynamic();
|
||||
};
|
||||
|
||||
const addStep_dynamic = () => {
|
||||
const stepFragment = stepTemplate_dynamic.content.cloneNode(true);
|
||||
const newStep = stepFragment.querySelector('.step'); // .step is class on root
|
||||
stepsContainer_dynamic.appendChild(newStep);
|
||||
|
||||
const currentStepIndex = stepsContainer_dynamic.querySelectorAll('.step').length - 1;
|
||||
addRequirement_dynamic(newStep, currentStepIndex); // Add one requirement by default
|
||||
|
||||
updateIndices_dynamic();
|
||||
};
|
||||
|
||||
stepsContainer_dynamic.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('removeStepBtn_dynamic')) {
|
||||
event.target.closest('.step').remove();
|
||||
if (stepsContainer_dynamic.querySelectorAll('.step').length === 0) {
|
||||
// addStep_dynamic(); // Optionally re-add a step if all are removed
|
||||
}
|
||||
updateIndices_dynamic();
|
||||
} else if (event.target.classList.contains('addRequirementBtn_dynamic')) {
|
||||
const stepElement = event.target.closest('.step');
|
||||
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
|
||||
addRequirement_dynamic(stepElement, stepIndex);
|
||||
} else if (event.target.classList.contains('removeRequirementBtn_dynamic')) {
|
||||
const requirementElement = event.target.closest('.requirement');
|
||||
const stepElement = event.target.closest('.step');
|
||||
const requirementsContainer = stepElement.querySelector('.requirementsContainer_dynamic');
|
||||
requirementElement.remove();
|
||||
|
||||
if (requirementsContainer.querySelectorAll('.requirement').length === 0) {
|
||||
// const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
|
||||
// addRequirement_dynamic(stepElement, stepIndex); // Optionally re-add a requirement
|
||||
const reqPlaceholder = requirementsContainer.querySelector('p.text-muted.small');
|
||||
if (reqPlaceholder) reqPlaceholder.style.display = 'block'; // Show placeholder if no reqs
|
||||
}
|
||||
updateIndices_dynamic();
|
||||
}
|
||||
});
|
||||
|
||||
addStepBtn_dynamic.addEventListener('click', addStep_dynamic);
|
||||
|
||||
// Add one step by default when the page loads, if no steps already (e.g. from server-side render)
|
||||
if (stepsContainer_dynamic.children.length === 1 && stepsContainer_dynamic.firstElementChild.tagName === 'P') { // Only placeholder present
|
||||
addStep_dynamic();
|
||||
}
|
||||
|
||||
form_dynamic.addEventListener('submit', (event) => {
|
||||
if (stepsContainer_dynamic.querySelectorAll('.step').length === 0) {
|
||||
alert('Please add at least one step to the flow.');
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const steps = stepsContainer_dynamic.querySelectorAll('.step');
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
if (steps[i].querySelectorAll('.requirementsContainer_dynamic .requirement').length === 0) {
|
||||
alert(`Step ${i + 1} must have at least one signature requirement.`);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('One or more elements for the dynamic step-by-step form were not found. JS not initialized.');
|
||||
}
|
||||
// --- End of Dynamic Step-by-Step Form Logic ---
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Reference in New Issue
Block a user