1use std::collections::HashMap;
11
12use topcoat::{
13 context::Cx,
14 runtime::{Event, Signal, signal},
15 view::*,
16};
17
18use super::{
19 Schema,
20 fields::{ChoiceField, CustomField, Field, TextField},
21 tree::Node,
22};
23use crate::{DeclarationErrorKind, topcoat_compat::async_page};
24
25pub trait Watched<F> {
32 #[doc(hidden)]
33 fn key(&self) -> &str;
34
35 #[doc(hidden)]
36 fn checkbox(&self) -> bool {
37 false
38 }
39}
40
41impl<F> Watched<F> for TextField<F> {
42 fn key(&self) -> &str {
43 self.name()
44 }
45}
46
47impl<F> Watched<F> for ChoiceField<F> {
48 fn key(&self) -> &str {
49 self.name()
50 }
51}
52
53impl<F> Watched<F> for CustomField<F> {
54 fn key(&self) -> &str {
55 self.name()
56 }
57
58 fn checkbox(&self) -> bool {
59 self.is_checkbox()
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub(crate) struct Condition {
66 watched: String,
67 checkbox: bool,
69 values: Vec<String>,
70}
71
72impl Condition {
73 pub(crate) fn new<F>(
74 watched: &impl Watched<F>,
75 values: impl IntoIterator<Item = impl Into<String>>,
76 ) -> Self {
77 Self {
78 watched: watched.key().to_string(),
79 checkbox: watched.checkbox(),
80 values: values.into_iter().map(Into::into).collect(),
81 }
82 }
83
84 fn holds(&self, values: &HashMap<String, String>) -> bool {
87 let posted = match values.get(&self.watched) {
88 Some(value) => value.trim(),
89 None if self.checkbox => "false",
90 None => "",
91 };
92 self.values.iter().any(|value| value == posted)
93 }
94
95 pub(crate) fn guard<'a>(
98 &self,
99 cx: &'a Cx,
100 scope: &str,
101 values: &HashMap<String, String>,
102 inner: BoxView<'a>,
103 ) -> BoxView<'a> {
104 let condition = self.clone();
105 let initial = self.holds(values);
106 let scope = scope.to_string();
107 async_page(async move {
108 let shown = shown(cx, &scope, &condition, initial);
109 let enabled = shown.clone();
110 Ok(view! {
111 cx =>
112 <fieldset
113 class="contents"
114 :disabled=$(!enabled.get())
115 :hidden=$(!shown.get())
116 >
117 (inner)
118 </fieldset>
119 })
120 })
121 }
122}
123
124fn shown(cx: &Cx, scope: &str, condition: &Condition, initial: bool) -> Signal<bool> {
131 let page = topcoat::context::try_request_context::<http::request::Parts>(cx)
132 .map(|parts| parts.uri.path().to_string())
133 .unwrap_or_default();
134 let values = condition.values.join("\u{1f}");
135 signal(
136 &cx.keyed((
137 "tablo-condition",
138 page,
139 scope,
140 condition.watched.as_str(),
141 values,
142 )),
143 move || initial,
144 )
145}
146
147pub(crate) fn watch<'a>(
155 cx: &'a Cx,
156 field: &Field,
157 conditions: &[Condition],
158 scope: &str,
159 values: &HashMap<String, String>,
160 control: BoxView<'a>,
161) -> BoxView<'a> {
162 let scope = scope.to_string();
163 let conditions: Vec<(Condition, bool)> = conditions
164 .iter()
165 .map(|condition| (condition.clone(), condition.holds(values)))
166 .collect();
167 let checkbox = field.is_checkbox();
168 let key = field.name().to_string();
169 async_page(async move {
170 let mut view = control;
171 for (condition, initial) in conditions {
172 let shown = shown(cx, &scope, &condition, initial);
173 if checkbox {
174 let on = condition.values.iter().any(|value| value == "true");
176 let off = condition.values.iter().any(|value| value == "false");
177 let name = key.clone();
178 view = view! {
179 cx =>
180 <div
181 class="contents"
182 @change=$(|e: Event| if e.target.name == name {
183 shown.set(if e.target.checked { on } else { off })
184 })
185 >
186 (view)
187 </div>
188 }
189 .boxed();
190 continue;
191 }
192 let mut values = condition.values.into_iter();
193 let Some(first) = values.next() else {
194 continue;
195 };
196 let (set, name) = (shown.clone(), key.clone());
197 view = view! {
198 cx =>
199 <div
200 class="contents"
201 @change=$(|e: Event| if e.target.name == name {
202 set.set(e.target.value.trim() == first)
203 })
204 >
205 (view)
206 </div>
207 }
208 .boxed();
209 for value in values {
210 let (set, name) = (shown.clone(), key.clone());
211 view = view! {
212 cx =>
213 <div
214 class="contents"
215 @change=$(|e: Event| if e.target.name == name {
216 if e.target.value.trim() == value {
217 set.set(true)
218 }
219 })
220 >
221 (view)
222 </div>
223 }
224 .boxed();
225 }
226 }
227 Ok(view)
228 })
229}
230
231impl Node {
232 fn condition<'s>(&'s self, fields: &'s [Field]) -> Option<&'s Condition> {
234 match self {
235 Node::Field(index) => fields[*index].condition(),
236 Node::Section(s) => s.condition.as_ref(),
237 Node::Group(g) => g.condition.as_ref(),
238 Node::Grid(g) => g.condition.as_ref(),
239 Node::Embedded(_) | Node::Unbound(_) => None,
240 }
241 }
242}
243
244#[derive(Default)]
246struct Scan<'s> {
247 fields: Vec<(usize, Vec<&'s Condition>)>,
249 conditions: Vec<(&'s Condition, Vec<&'s Condition>)>,
251}
252
253impl<'s> Scan<'s> {
254 fn of(nodes: &'s [Node], fields: &'s [Field]) -> Self {
255 let mut scan = Self::default();
256 scan.walk(nodes, fields, &mut Vec::new());
257 scan
258 }
259
260 fn walk(&mut self, nodes: &'s [Node], fields: &'s [Field], chain: &mut Vec<&'s Condition>) {
261 for node in nodes {
262 let own = node.condition(fields);
263 if let Some(condition) = own {
264 self.conditions.push((condition, chain.clone()));
265 chain.push(condition);
266 }
267 match node {
268 Node::Field(index) => self.fields.push((*index, chain.clone())),
269 Node::Embedded(embedded) => {
270 embedded.visit_fields(&mut |index| self.fields.push((index, chain.clone())));
271 }
272 node => self.walk(node.children().unwrap_or_default(), fields, chain),
273 }
274 if own.is_some() {
275 chain.pop();
276 }
277 }
278 }
279
280 fn guarding(&self, fields: &[Field], key: &str) -> Option<&[&'s Condition]> {
282 self.fields
283 .iter()
284 .find(|(index, _)| fields[*index].name() == key)
285 .map(|(_, chain)| chain.as_slice())
286 }
287}
288
289impl<F> Schema<F> {
290 pub(crate) fn condition_hidden(&self, values: &HashMap<String, String>) -> Vec<String> {
292 Scan::of(&self.nodes, &self.fields)
293 .fields
294 .into_iter()
295 .filter(|(_, chain)| chain.iter().any(|condition| !condition.holds(values)))
296 .map(|(index, _)| self.fields[index].name().to_string())
297 .collect()
298 }
299
300 pub(crate) fn watched(&self) -> HashMap<String, Vec<Condition>> {
302 let mut out: HashMap<String, Vec<Condition>> = HashMap::new();
303 for (condition, _) in Scan::of(&self.nodes, &self.fields).conditions {
304 let driven = out.entry(condition.watched.clone()).or_default();
305 if !driven.contains(condition) {
306 driven.push(condition.clone());
307 }
308 }
309 out
310 }
311
312 pub(crate) fn condition_errors(&self) -> Vec<DeclarationErrorKind> {
316 let scan = Scan::of(&self.nodes, &self.fields);
317 let mut errors = Vec::new();
318 for (condition, guarding) in &scan.conditions {
319 let watched = condition.watched.clone();
320 match scan.guarding(&self.fields, &condition.watched) {
321 None => errors.push(DeclarationErrorKind::UnplacedWatchedField {
322 field: watched.clone(),
323 }),
324 Some(chain)
328 if !chain
329 .iter()
330 .all(|hiding| guarding.iter().any(|c| std::ptr::eq(*c, *hiding))) =>
331 {
332 errors.push(DeclarationErrorKind::HiddenWatchedField {
333 field: watched.clone(),
334 });
335 }
336 Some(_) => {}
337 }
338 if let Some(field) = self.fields.iter().find(|f| f.name() == condition.watched) {
339 for value in &condition.values {
340 if field.can_post(value) == Some(false) {
341 errors.push(DeclarationErrorKind::UnpostedConditionValue {
342 field: watched.clone(),
343 value: value.clone(),
344 });
345 }
346 }
347 }
348 }
349 for (index, chain) in &scan.fields {
350 let field = &self.fields[*index];
351 if !chain.is_empty() && field.is_required() {
352 errors.push(DeclarationErrorKind::RequiredConditionalField {
353 field: field.name().to_string(),
354 });
355 }
356 }
357 errors
358 }
359}
360
361#[cfg(test)]
362mod tests;