diff --git a/src/templates/helpers.rs b/src/templates/helpers.rs index c560e012d..2bb6d2122 100644 --- a/src/templates/helpers.rs +++ b/src/templates/helpers.rs @@ -6,9 +6,11 @@ use serde_json::Value; pub mod merge; pub mod uniq_lines; +pub mod filter; use merge::register_merge; use uniq_lines::register_uniq_lines; +use filter::register_filter; pub fn register_helpers<'a>(h: Handlebars<'a>) -> Handlebars<'a> { let h = register_concat_helper(h); @@ -19,6 +21,7 @@ pub fn register_helpers<'a>(h: Handlebars<'a>) -> Handlebars<'a> { let h = register_pluralize_helpers(h); let h = register_merge(h); let h = register_uniq_lines(h); + let h = register_filter(h); h } diff --git a/src/templates/helpers/filter.rs b/src/templates/helpers/filter.rs new file mode 100644 index 000000000..aa854c1ac --- /dev/null +++ b/src/templates/helpers/filter.rs @@ -0,0 +1,157 @@ +use handlebars::{ + Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, + ScopedJson, +}; +use serde_json::{json, Value, Map}; + +#[derive(Clone, Copy)] +pub struct FilterHelper; + +/// A Handlebars helper to filter an iterable JSON value. +/// It receives the value to be filtered and a string containing the condition predicate, +/// then uses Handlebars' truthy logic to filter the items in the value. +/// It also supports the `#if` helper's `includeZero` optional parameter. +impl HelperDef for FilterHelper { + fn call_inner<'reg: 'rc, 'rc>( + &self, + h: &Helper<'reg, 'rc>, + r: &'reg Handlebars<'reg>, + _ctx: &'rc Context, + _rc: &mut RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + let mut params = h.params().iter(); + let value = params + .next() + .ok_or(RenderError::new( + "Filter helper: Param not found for index 0; must be value to be filtered", + ))? + .value(); + + let condition = params + .next() + .ok_or(RenderError::new("Filter helper: Param not found for index 1; must be string containing filter condition predicate"))? + .value() + .as_str() + .ok_or(RenderError::new("Filter helper: filter condition predicate must be a string"))?; + + let include_zero = h + .hash_get("includeZero") + .and_then(|v| v.value().as_bool()) + .unwrap_or(false); + + // This template allows us to evaluate the condition according to + // Handlebars' available context/property logic, helper functions, and + // truthiness logic. + let template = format!( + "{}{}{}{}", + "{{#if ", + condition, + include_zero.then_some(" includeZero=true").unwrap_or(""), + "}}true{{else}}false{{/if}}" + ); + + match value { + Value::Array(items) => { + let mut filtered_array = vec![]; + for item in items.iter() { + match r.render_template(&template, &item) { + Ok(s) => { + if s.as_str() == "true" { + filtered_array.push(item); + } + }, + Err(e) => { + return Err(e); + } + } + } + Ok(ScopedJson::Derived(json!(filtered_array))) + }, + Value::Object(object) => { + let mut filtered_object = Map::new(); + for key in object.clone().keys() { + if let Some(v) = object.get(key) { + match r.render_template(&template, &v) { + Ok(s) => { + if s.as_str() == "true" { + filtered_object.insert(key.into(), v.clone()); + } + }, + Err(e) => { + return Err(e); + } + } + } + } + Ok(ScopedJson::Derived(json!(filtered_object))) + }, + _ => Err(RenderError::new("Filter helper: value to be filtered must be an array or object")) + } + } +} + +pub fn register_filter<'a>(mut h: Handlebars<'a>) -> Handlebars<'a> { + h.register_helper("filter", Box::new(FilterHelper)); + + h +} + +#[cfg(test)] +mod tests { + use handlebars::Handlebars; + use serde_json::json; + use crate::templates::helpers::register_filter; + + fn setup_handlebars<'a>() -> Handlebars<'a> { + let hbs = Handlebars::new(); + let hbs = register_filter(hbs); + hbs + } + + #[test] + fn respects_include_zero() { + let hbs = setup_handlebars(); + let value = json!([0, 1, 0, 2, 0, 3, 0, 4, 0, 5]); + // The predicate filters out zeroes. + let template = "{{#each (filter this \"this\")}}{{this}}{{/each}}"; + match hbs.render_template(&template, &value) { + Ok(s) => assert_eq!(s, "12345", "`filter` helper did not filter out falsy zero"), + Err(e) => panic!("{}", e) + } + // This predicate, however, does not. + let template = "{{#each (filter this \"this\" includeZero=true)}}{{this}}{{/each}}"; + match hbs.render_template(&template, &value) { + Ok(s) => assert_eq!(s, "0102030405", "`filter` helper did not treat zero as truthy"), + Err(e) => panic!("{}", e) + } + } + + #[test] + fn can_filter_object_by_value() { + let hbs = setup_handlebars(); + let value = json!({"name": "Alice", "age": 24, "wild": false, "species": "iguana"}); + // The predicate filters out the 'wild' property. + let template = "{{#each (filter this \"this\")}}{{@key}}: {{this}}, {{/each}}"; + match hbs.render_template(&template, &value) { + Ok(s) => assert_eq!(s, "name: Alice, age: 24, species: iguana, ", "`filter` helper did not filter object key/value pairs by value"), + Err(e) => panic!("{}", e) + } + } + + + #[test] + fn can_filter_complex_value() { + let hbs = setup_handlebars(); + let value = json!([ + {"name": "Alice", "age": 24, "wild": true, "species": "iguana"}, + {"name": "Bob", "age": 3, "wild": false, "species": "hamster"}, + {"name": "Carol", "age": 1, "wild": true, "species": "octopus"} + ]); + // The predicate filters out domestic animals. + let template = "{{#each (filter this \"wild\")}}{{name}} the {{species}} is {{age}}. {{/each}}"; + match hbs.render_template(&template, &value) { + Ok(s) => assert_eq!(s, "Alice the iguana is 24. Carol the octopus is 1. ", "`filter` helper did not operate on a list full of complex values"), + Err(e) => panic!("{}", e) + } + } +} \ No newline at end of file diff --git "a/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" "b/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" index 5e8ec4dea..288a5c795 100644 --- "a/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" +++ "b/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" @@ -39,7 +39,7 @@ let {{camel_case field_name}}: Array<{{> (concat field_type.type "/type")}} | un let errorSnackbar: Snackbar; -$: {{#each entry_type.fields}}{{#if widget}}{{camel_case field_name}}{{#unless @last}}, {{/unless}}{{/if}}{{/each}}; +$: {{#each (filter entry_type.fields "widget")}}{{camel_case field_name}}{{#unless @last}}, {{/unless}}{{/each}}; $: is{{pascal_case entry_type.name}}Valid = true{{#each entry_type.fields}}{{#if widget}}{{#if (eq cardinality "single")}} && {{> (concat field_type.type "/" widget "/is-valid") variable_to_validate=(camel_case field_name) }}{{/if}}{{#if (eq cardinality "vector")}} && {{camel_case field_name}}.every(e => {{> (concat field_type.type "/" widget "/is-valid") variable_to_validate="e" }}){{/if}}{{/if}}{{/each}}; onMount(() => { @@ -55,7 +55,7 @@ onMount(() => { async function update{{pascal_case entry_type.name}}() { - const {{camel_case entry_type.name}}: {{pascal_case entry_type.name}} = { + const {{camel_case entry_type.name}}: {{pascal_case entry_type.name}} = { {{#each entry_type.fields}} {{#if widget}} {{#if (eq cardinality "single") }} @@ -86,7 +86,7 @@ async function update{{pascal_case entry_type.name}}() { updated_{{snake_case entry_type.name}}: {{camel_case entry_type.name}} } }); - + dispatch('{{kebab_case entry_type.name}}-updated', { actionHash: updateRecord.signed_action.hashed.hash }); } catch (e) { errorSnackbar.labelText = `Error updating the {{lower_case entry_type.name}}: ${e.data.data}`; @@ -99,7 +99,7 @@ async function update{{pascal_case entry_type.name}}() {
Edit {{pascal_case entry_type.name}} - + {{#each entry_type.fields}} {{#if widget}}
@@ -108,7 +108,7 @@ async function update{{pascal_case entry_type.name}}() { {{else}} {{> Vec/edit/render field_name=field_name field_type=field_type widget=widget }} {{/if}} - +
{{/if}} @@ -121,7 +121,7 @@ async function update{{pascal_case entry_type.name}}() { on:click={() => dispatch('edit-canceled')} style="flex: 1; margin-right: 16px" > -