//! Code connect: components linked to their code components show the real snippet, with the //! instance's values, instead of generated code. use crate::components::tests::{ex, idv}; use crate::params::id_of; use crate::{EngineError, Session}; use figcraft_doc::*; use serde_json::{Value, json}; fn code(s: &mut Session, id: NodeId, lang: &str) -> String { ex(s, "inspect.code", json!({"id": id, "language": lang}))["code"].as_str().unwrap_or_default().to_string() } /// A "Card" component with a "Title" text property and a "Show dot" boolean property. fn card(s: &mut Session) -> NodeId { let f = idv(&ex(s, "frame.create", json!({"x": 0, "y": 0, "width": 200, "height": 100, "name": "Card"}))); let title = idv(&ex(s, "text.create", json!({"text": "Hello", "x": 10, "y": 10, "parent": f, "props": {"name": "Title"}}))); let dot = idv(&ex(s, "shape.ellipse", json!({"x": 170, "y": 10, "width": 20, "height": 20, "parent": f, "props": {"name": "Dot"}}))); let c = idv(&ex(s, "component.create", json!({"ids": [f]}))); ex(s, "component.addProperty", json!({"id": dot, "name": "Show dot", "type": "boolean", "default": true})); ex(s, "component.addProperty", json!({"id": c, "name": "Title", "type": "text", "layer": title})); c } /// A "Button" set with State=Default|Hover. Returns (set, hover variant). fn button(s: &mut Session) -> (NodeId, NodeId) { let variants: Vec = ["Default", "Hover"] .iter() .enumerate() .map(|(k, state)| { let f = idv(&ex(s, "frame.create", json!({"x": k as f64 * 150.0, "y": 300, "width": 100, "height": 40, "name": format!("State={state}")}))); idv(&ex(s, "component.create", json!({"ids": [f]}))) }) .collect(); let set = idv(&ex(s, "component.combineVariants", json!({"ids": variants.clone()}))); ex(s, "node.set", json!({"id": set, "props": {"name": "Button"}})); (set, variants[1]) } #[test] fn connected_instances_show_the_real_snippet() { let mut s = Session::new(); let c = card(&mut s); let i = idv(&ex(&mut s, "instance.create", json!({"component": c, "x": 300, "y": 0}))); ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Show dot": false, "Title": "Buy"}})); let template = "{Show dot ? \"•\" : \"\"}"; let v = ex(&mut s, "codeConnect.set", json!({"id": i, "importPath": "@acme/ui", "template": template})); assert_eq!(id_of(&v["component"]), Some(c), "stored on the main"); assert_eq!(v["unknown"], json!(["onClick"]), "code-only names are kept as written"); let want = "import { Card } from \"@acme/ui\";\n\n\n"; assert_eq!(v["code"], want); assert_eq!(code(&mut s, i, "react"), want); assert_eq!(ex(&mut s, "inspect.code", json!({"id": i, "language": "react"}))["connected"], true); // The main itself: the defaults. Instances never carry links themselves. assert_eq!(code(&mut s, c, "react"), "import { Card } from \"@acme/ui\";\n\n•\n"); assert!(s.doc().unwrap().node(i).unwrap().code_links.is_empty()); // Escaped per language: a JSX string with escapes is an expression. ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Say \"hi\""}})); assert!(code(&mut s, i, "react").contains("title={\"Say \\\"hi\\\"\"}")); ex( &mut s, "codeConnect.set", json!({"id": c, "language": "swiftui", "importPath": "AcmeUI", "template": "Card(title: {title}, style: {Show dot ? \".dotted\" : \".plain\"})"}), ); assert_eq!(code(&mut s, i, "swiftui"), "import AcmeUI\n\nCard(title: \"Say \\\"hi\\\"\", style: .plain)\n"); assert!(code(&mut s, i, "compose").contains("@Composable"), "other languages stay generated"); // Leptos: a `use` line and `view!` markup (strings with escapes in braces). ex( &mut s, "codeConnect.set", json!({"id": c, "language": "leptos", "importPath": "crate::ui", "template": ""}), ); assert_eq!(code(&mut s, i, "leptos"), "use crate::ui::Card;\n\n\n"); assert!(s.execute("codeConnect.set", &json!({"id": c, "language": "leptos", "importPath": "crate/ui"})).is_err(), "a Rust path"); ex(&mut s, "codeConnect.remove", json!({"id": c, "language": "leptos"})); // inspect.componentCode: the snippet as the usage, and the link. let v = ex(&mut s, "inspect.componentCode", json!({"id": i, "language": "swiftui"})); assert_eq!(v["usage"], "Card(title: \"Say \\\"hi\\\"\", style: .plain)\n"); assert_eq!(v["connected"]["import"], "import AcmeUI"); assert_eq!(ex(&mut s, "inspect.componentCode", json!({"id": i, "language": "compose"}))["connected"], Value::Null); // Inside a generated layer tree, connected instances are their snippet. let screen = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 500, "width": 400, "height": 300, "name": "Screen"}))); ex(&mut s, "instance.create", json!({"component": c, "x": 20, "y": 30, "parent": screen})); assert!(code(&mut s, screen, "react").contains("•\n")); // Literal braces stay; fields left out keep their value. ex(&mut s, "codeConnect.set", json!({"id": c, "template": ""})); assert_eq!(code(&mut s, c, "react"), "import { Card } from \"@acme/ui\";\n\n\n"); // Validation. for bad in [ json!({"id": c, "template": "{title ? \"a\"}"}), json!({"id": c, "template": " "}), json!({"id": c, "name": "1Card"}), json!({"id": c, "importPath": "a\"b"}), json!({"id": c, "language": "swiftui", "importPath": "Acme UI"}), json!({"id": c, "language": "html"}), json!({"id": c, "template": 5}), json!({"id": screen}), ] { assert!(s.execute("codeConnect.set", &bad).is_err(), "{bad}"); } // Disconnect one language; undo brings it back. ex(&mut s, "codeConnect.remove", json!({"id": i, "language": "react"})); assert!(code(&mut s, i, "react").contains("export function Card({")); assert!(code(&mut s, i, "swiftui").starts_with("import AcmeUI")); ex(&mut s, "edit.undo", json!({})); assert!(code(&mut s, i, "react").starts_with("import { Card }")); assert_eq!(ex(&mut s, "codeConnect.remove", json!({"id": c}))["removed"], 2); assert!(s.execute("codeConnect.remove", &json!({"id": c})).is_err(), "nothing left"); } #[test] fn sets_connect_through_the_set_and_round_trip_as_json() { let mut s = Session::new(); let (set, hover) = button(&mut s); let v = ex(&mut s, "codeConnect.set", json!({"id": hover})); assert_eq!(id_of(&v["component"]), Some(set)); ex(&mut s, "codeConnect.set", json!({"id": set, "language": "swiftui"})); let i = idv(&ex(&mut s, "instance.create", json!({"component": hover, "x": 0, "y": 500}))); assert_eq!(code(&mut s, i, "react"), "