From f5cb51419005a3ada239e81d3661930eba1c9576 Mon Sep 17 00:00:00 2001 From: Zachary Hamm Date: Thu, 12 Dec 2024 13:46:47 -0600 Subject: [PATCH] feat: generate templates from selected components Takes a set of component ids, and a view, and generates a management function that will recreate the components on demand, with their (relative) position, connections, and parentage preserved. Then attaches that function to a brand new schema variant. --- .../ModelingView/TemplateSelectionModal.vue | 20 +- app/web/src/store/components.store.ts | 23 +- lib/dal/src/component.rs | 16 +- lib/dal/src/management/generator.rs | 405 ++++++++++++++++++ lib/dal/src/management/mod.rs | 11 +- lib/dal/src/management/prototype.rs | 14 + lib/dal/tests/integration_test/management.rs | 2 + .../integration_test/management/generator.rs | 106 +++++ lib/sdf-server/src/service/v2/management.rs | 20 +- .../v2/management/generate_template.rs | 105 +++++ 10 files changed, 695 insertions(+), 27 deletions(-) create mode 100644 lib/dal/src/management/generator.rs create mode 100644 lib/dal/tests/integration_test/management/generator.rs create mode 100644 lib/sdf-server/src/service/v2/management/generate_template.rs diff --git a/app/web/src/components/ModelingView/TemplateSelectionModal.vue b/app/web/src/components/ModelingView/TemplateSelectionModal.vue index 8f5099635d..e74114ab72 100644 --- a/app/web/src/components/ModelingView/TemplateSelectionModal.vue +++ b/app/web/src/components/ModelingView/TemplateSelectionModal.vue @@ -81,11 +81,7 @@ import { useComponentsStore } from "@/store/components.store"; import { useViewsStore } from "@/store/views.store"; import { useFeatureFlagsStore } from "@/store/feature_flags.store"; import ComponentCard from "../ComponentCard.vue"; -import { - DiagramGroupData, - DiagramNodeData, - DiagramViewData, -} from "../ModelingDiagram/diagram_types"; +import { DiagramViewData } from "../ModelingDiagram/diagram_types"; const componentsStore = useComponentsStore(); const viewsStore = useViewsStore(); @@ -147,15 +143,19 @@ onBeforeUnmount(() => { }); const onCreateTemplate = () => { - if (!readyToSubmit.value || !validSelectedComponents.value) return; + if ( + !readyToSubmit.value || + !validSelectedComponents.value || + !viewsStore.selectedViewId + ) + return; const templateData = { - assetColor: assetColor.value, + color: assetColor.value, assetName: assetName.value, funcName: funcName.value, - components: selectedComponents.value as Array< - DiagramNodeData | DiagramGroupData - >, + componentIds: selectedComponents.value.map((component) => component.def.id), + viewId: viewsStore.selectedViewId, }; componentsStore.CREATE_TEMPLATE_FUNC_FROM_COMPONENTS(templateData); diff --git a/app/web/src/store/components.store.ts b/app/web/src/store/components.store.ts index 4a110c8deb..e0bd129773 100644 --- a/app/web/src/store/components.store.ts +++ b/app/web/src/store/components.store.ts @@ -37,6 +37,7 @@ import { Resource } from "@/api/sdf/dal/resource"; import { CodeView } from "@/api/sdf/dal/code_view"; import ComponentUpgrading from "@/components/toasts/ComponentUpgrading.vue"; import { nonNullable } from "@/utils/typescriptLinter"; +import { ViewId } from "@/api/sdf/dal/views"; import handleStoreError from "./errors"; import { useChangeSetsStore } from "./change_sets.store"; import { useAssetStore } from "./asset.store"; @@ -1163,14 +1164,26 @@ export const useComponentsStore = (forceChangeSetId?: ChangeSetId) => { }, async CREATE_TEMPLATE_FUNC_FROM_COMPONENTS(templateData: { - assetColor: string; + color: string; assetName: string; funcName: string; - components: Array; + componentIds: ComponentId[]; + viewId: ViewId; }) { - // TODO(Wendy) - this is where the end point would be called! - // eslint-disable-next-line no-console - console.log(templateData); + const { color, assetName, funcName, componentIds, viewId } = + templateData; + + return new ApiRequest({ + method: "post", + url: `v2/workspaces/${workspaceId}/change-sets/${changeSetId}/management/generate_template/${viewId}`, + params: { + componentIds, + assetName, + funcName, + category: "Templates", + color, + }, + }); }, setComponentDisplayName( diff --git a/lib/dal/src/component.rs b/lib/dal/src/component.rs index fe0d0c7106..a232bf698c 100644 --- a/lib/dal/src/component.rs +++ b/lib/dal/src/component.rs @@ -1465,16 +1465,16 @@ impl Component { &mut self, ctx: &DalContext, view_id: ViewId, - x: impl Into, - y: impl Into, - width: Option>, - height: Option>, + x: isize, + y: isize, + width: Option, + height: Option, ) -> ComponentResult { let new_geometry = RawGeometry { - x: x.into(), - y: y.into(), - width: width.map(|w| w.into()), - height: height.map(|h| h.into()), + x, + y, + width, + height, }; self.set_raw_geometry(ctx, new_geometry, view_id).await diff --git a/lib/dal/src/management/generator.rs b/lib/dal/src/management/generator.rs new file mode 100644 index 0000000000..6f855b5bbe --- /dev/null +++ b/lib/dal/src/management/generator.rs @@ -0,0 +1,405 @@ +use std::collections::{HashMap, HashSet, VecDeque}; + +use si_frontend_types::RawGeometry; +use si_id::{ComponentId, SchemaId, ViewId}; + +use crate::management::{ + ConnectionIdentifier, ManagementConnection, ManagementCreateOperation, ManagementGeometry, + IGNORE_PATHS, +}; +use crate::prop::PropPath; +use crate::{AttributeValue, Component, DalContext, InputSocket, OutputSocket, Prop, PropKind}; + +use super::{ManagementCreateOperations, ManagementResult}; + +pub async fn generate_template( + ctx: &DalContext, + view_id: ViewId, + component_ids: &[ComponentId], +) -> ManagementResult<(ManagementCreateOperations, Vec)> { + #[derive(Debug, Clone)] + struct ConnectionInfo { + from_socket_name: String, + to_component_id: ComponentId, + to_socket_name: String, + } + + #[derive(Debug, Clone)] + struct ComponentInfo { + component_id: ComponentId, + component: Component, + kind: String, + placeholder: String, + } + + // We have to gather up a bunch of data here and make two passes over the components + let mut components = Vec::new(); + let mut outgoing_connections: HashMap> = HashMap::new(); + let mut geometries = HashMap::new(); + let mut placeholders_by_component_id = HashMap::new(); + let mut placeholder_set = HashSet::new(); + let mut input_socket_names = HashMap::new(); + let mut output_socket_names = HashMap::new(); + let mut schema_names = HashMap::new(); + + for &component_id in component_ids { + let component = Component::get_by_id(ctx, component_id).await?; + let schema = Component::schema_for_component_id(ctx, component_id).await?; + let schema_name = schema_names + .entry(schema.id()) + .or_insert_with(|| schema.name().to_string()) + .to_owned(); + + let mut geometry = component.geometry(ctx, view_id).await?.into_raw(); + + // We want to be sure that frames, and only frames, always have a height/width, while + // components never have one. + let component_type = component.get_type(ctx).await?; + if component_type.is_frame() && geometry.width.zip(geometry.height).is_none() { + geometry.width = Some(500); + geometry.height = Some(500); + } else if !component_type.is_frame() + && (geometry.width.is_some() || geometry.height.is_some()) + { + geometry.width = None; + geometry.height = None; + } + + for incoming_connection in component.incoming_connections(ctx).await? { + let to_socket_name = match input_socket_names + .get(&incoming_connection.to_input_socket_id) + .cloned() + { + Some(name) => name, + None => { + let socket = + InputSocket::get_by_id(ctx, incoming_connection.to_input_socket_id).await?; + let name = socket.name(); + input_socket_names + .insert(incoming_connection.to_input_socket_id, name.to_string()); + name.to_string() + } + }; + + let from_socket_name = match output_socket_names + .get(&incoming_connection.from_output_socket_id) + .cloned() + { + Some(name) => name, + None => { + let socket = + OutputSocket::get_by_id(ctx, incoming_connection.from_output_socket_id) + .await?; + let name = socket.name(); + output_socket_names + .insert(incoming_connection.from_output_socket_id, name.to_string()); + name.to_string() + } + }; + + let outgoing = ConnectionInfo { + from_socket_name, + to_socket_name, + to_component_id: incoming_connection.to_component_id, + }; + + outgoing_connections + .entry(incoming_connection.from_component_id) + .and_modify(|conns| conns.push(outgoing.clone())) + .or_insert_with(|| vec![outgoing]); + } + + let name = component.name(ctx).await?; + let placeholder = make_placeholder(&name, &placeholder_set).await; + placeholder_set.insert(placeholder.clone()); + placeholders_by_component_id.insert(component_id, placeholder.clone()); + + components.push(ComponentInfo { + component_id, + component, + kind: schema_name, + placeholder, + }); + geometries.insert(component_id, geometry); + } + + let (origin_x, origin_y) = calculate_top_and_center(&geometries).await; + + let mut creates: HashMap = HashMap::new(); + + for ComponentInfo { + component_id, + component, + kind, + placeholder, + } in components + { + // get parentage + let parent = component + .parent(ctx) + .await? + .and_then(|parent_id| placeholders_by_component_id.get(&parent_id).cloned()); + + let mut connections = vec![]; + if let Some(conns) = outgoing_connections.remove(&component_id) { + for conn in conns { + if let Some(to_placeholder) = + placeholders_by_component_id.get(&conn.to_component_id) + { + connections.push(ManagementConnection { + from: conn.from_socket_name, + to: ConnectionIdentifier { + component: to_placeholder.to_owned(), + socket: conn.to_socket_name, + }, + }) + } + } + } + + let geometry: Option = + geometries.get(&component_id).cloned().map(|mut geo| { + geo.x -= origin_x; + geo.y -= origin_y; + geo.into() + }); + + let connect = if connections.is_empty() { + None + } else { + Some(connections) + }; + + let properties = component.view(ctx).await?; + let properties = if let Some(mut properties) = properties { + let remove_paths = calculate_paths_to_remove(ctx, component.id(), &properties).await?; + for path in remove_paths { + let path_as_refs: Vec<_> = path.iter().skip(1).map(|s| s.as_str()).collect(); + remove_value_at_path(&mut properties, &path_as_refs); + } + + for remove_path in IGNORE_PATHS { + let path_as_refs: Vec<_> = remove_path.iter().skip(1).copied().collect(); + remove_value_at_path(&mut properties, &path_as_refs); + } + + Some(properties) + } else { + None + }; + + let create = ManagementCreateOperation { + kind: Some(kind), + properties, + geometry, + connect, + parent, + }; + + creates.insert(placeholder, create); + } + + Ok(( + creates, + schema_names.keys().map(ToOwned::to_owned).collect(), + )) +} + +async fn make_placeholder(name: &str, placeholders: &HashSet) -> String { + let mut cursor = name.to_string(); + loop { + if !placeholders.contains(&cursor) { + return cursor; + } + + let mut whitespace_split = cursor.rsplitn(2, ' '); + cursor = match whitespace_split + .next() + .and_then(|last_split| last_split.parse::().ok()) + .zip(whitespace_split.next()) + { + Some((number, before_split)) => { + if number > 0 { + format!("{before_split} {}", number.wrapping_add(1)) + } else { + format!("{before_split} {number} 2") + } + } + None => format!("{cursor} 2"), + }; + + tokio::task::yield_now().await; + } +} + +async fn calculate_paths_to_remove( + ctx: &DalContext, + component_id: ComponentId, + properties: &serde_json::Value, +) -> ManagementResult>> { + let variant_id = Component::schema_variant_id(ctx, component_id).await?; + + // walk the properties serde_json::Value object without recursion + let mut work_queue = VecDeque::new(); + work_queue.push_back((vec!["root".to_string()], properties)); + + let mut result = vec![]; + + while let Some((path, current_val)) = work_queue.pop_front() { + let path_as_refs: Vec<_> = path.iter().map(|part| part.as_str()).collect(); + if IGNORE_PATHS.contains(&path_as_refs.as_slice()) { + continue; + } + + let Some(prop_id) = + Prop::find_prop_id_by_path_opt(ctx, variant_id, &PropPath::new(path.as_slice())) + .await? + else { + continue; + }; + + let path_attribute_value_id = + Component::attribute_value_for_prop_id(ctx, component_id, prop_id).await?; + + if AttributeValue::is_set_by_dependent_function(ctx, path_attribute_value_id).await? { + result.push(path_as_refs.iter().map(|&s| s.to_string()).collect()); + continue; + } + + let prop = Prop::get_by_id(ctx, prop_id).await?; + + match prop.kind { + PropKind::Object => { + let serde_json::Value::Object(obj) = current_val else { + continue; + }; + + for (key, value) in obj { + let mut new_path = path.clone(); + new_path.push(key.to_owned()); + work_queue.push_back((new_path, value)); + } + } + PropKind::Map => { + let map_children = + AttributeValue::map_children(ctx, path_attribute_value_id).await?; + + for (key, child_id) in map_children { + if AttributeValue::is_set_by_dependent_function(ctx, child_id).await? { + let mut path: Vec = + path_as_refs.iter().map(|&s| s.to_string()).collect(); + path.push(key); + result.push(path); + } + } + } + _ => {} + } + } + + Ok(result) +} + +pub async fn calculate_top_and_center( + geometries: &HashMap, +) -> (isize, isize) { + let mut topmost: Option = None; + let mut leftmost: Option = None; + let mut rightmost: Option = None; + + for geometry in geometries.values() { + let current_topmost = topmost.unwrap_or(geometry.y); + if geometry.y <= current_topmost { + topmost.replace(geometry.y); + } + + let x = geometry.x - (geometry.width.unwrap_or(0) / 2); + + let current_leftmost = leftmost.unwrap_or(x); + if x <= current_leftmost { + leftmost.replace(x); + } + + let component_right_edge = geometry.x + + (geometry + .width + .map(|width| if width > 200 { width - 200 } else { 0 }) + .unwrap_or(0) + / 2); + + let current_rightmost = rightmost.unwrap_or(component_right_edge); + if component_right_edge >= current_rightmost { + rightmost.replace(component_right_edge); + } + + tokio::task::yield_now().await; + } + + let Some((topmost, (leftmost, rightmost))) = topmost.zip(leftmost.zip(rightmost)) else { + return (0, 0); + }; + + (leftmost + ((rightmost - leftmost).abs() / 2), topmost - 500) +} + +fn remove_value_at_path(from: &mut serde_json::Value, remove_path: &[&str]) { + if let Some(serde_json::Value::Object(ref mut obj)) = remove_path + .iter() + .take(remove_path.len() - 1) + .try_fold(from, |val, path_part| match *val { + serde_json::Value::Object(ref mut obj) => obj.get_mut(*path_part), + _ => None, + }) + { + if let Some(&key) = remove_path.iter().last() { + obj.remove_entry(key); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn removes_value_at_path() { + let mut value = serde_json::json!({ + "a": { "b": { "c": "d", "e": { "f": "g"}}} + }); + + let mut value_clone = value.clone(); + + remove_value_at_path(&mut value_clone, &["a", "b", "e", "f"]); + + assert_eq!( + serde_json::json!({ + "a": { "b": { "c": "d", "e": {}}} + }), + value_clone + ); + + remove_value_at_path(&mut value, &["a", "b", "c"]); + assert_eq!( + serde_json::json!({ + "a": { "b": { "e": { "f": "g"}}} + }), + value + ); + } + + #[tokio::test] + async fn makes_placeholders() { + let mut placeholder_set = HashSet::new(); + let base_name = "a bcd ef g -10"; + for i in 1..100 { + let placeholder = make_placeholder(base_name, &placeholder_set).await; + assert!(!placeholder_set.contains(&placeholder)); + if i == 1 { + assert_eq!(base_name, placeholder.as_str()); + } else { + assert_eq!(format!("{base_name} {i}"), placeholder); + } + placeholder_set.insert(placeholder); + } + } +} diff --git a/lib/dal/src/management/mod.rs b/lib/dal/src/management/mod.rs index acd906e365..848fe37bd3 100644 --- a/lib/dal/src/management/mod.rs +++ b/lib/dal/src/management/mod.rs @@ -32,6 +32,7 @@ use crate::{ }; use crate::{EdgeWeightKind, WorkspaceSnapshotError}; +pub mod generator; pub mod prototype; #[derive(Debug, Error)] @@ -214,12 +215,16 @@ pub struct ManagementActionOperation { remove: Option>, } +pub type ManagementCreateOperations = HashMap; +pub type ManagementUpdateOperations = HashMap; +pub type ManagementActionOperations = HashMap; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ManagementOperations { - create: Option>, - update: Option>, - actions: Option>, + create: Option, + update: Option, + actions: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/lib/dal/src/management/prototype.rs b/lib/dal/src/management/prototype.rs index 17bc4b5430..e5a28cbbeb 100644 --- a/lib/dal/src/management/prototype.rs +++ b/lib/dal/src/management/prototype.rs @@ -636,6 +636,20 @@ impl ManagementPrototype { Ok(None) } + + pub async fn set_managed_schemas( + self, + ctx: &DalContext, + managed_schemas: Option>, + ) -> ManagementPrototypeResult<()> { + self.modify(ctx, |proto| { + proto.managed_schemas = managed_schemas.map(|schemas| schemas.into_iter().collect()); + Ok(()) + }) + .await?; + + Ok(()) + } } #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] diff --git a/lib/dal/tests/integration_test/management.rs b/lib/dal/tests/integration_test/management.rs index 4d347b2797..ae663a5f80 100644 --- a/lib/dal/tests/integration_test/management.rs +++ b/lib/dal/tests/integration_test/management.rs @@ -15,6 +15,8 @@ use dal_test::{ }; use veritech_client::ManagementFuncStatus; +pub mod generator; + #[test] async fn update_managed_components_in_view(ctx: &DalContext) { let small_odd_lego = create_component_for_default_schema_name_in_default_view( diff --git a/lib/dal/tests/integration_test/management/generator.rs b/lib/dal/tests/integration_test/management/generator.rs new file mode 100644 index 0000000000..dab3aa3332 --- /dev/null +++ b/lib/dal/tests/integration_test/management/generator.rs @@ -0,0 +1,106 @@ +use std::collections::HashMap; + +use dal::{diagram::view::View, Component, ComponentId, ComponentType, DalContext}; +use dal_test::{helpers::create_component_for_default_schema_name_in_default_view, test}; + +#[test] +async fn calculates_top_and_center(ctx: &DalContext) { + let default_view_id = View::get_id_for_default(ctx) + .await + .expect("get default view id"); + + let mut center_lego = create_component_for_default_schema_name_in_default_view( + ctx, + "Docker Image", + "center lego", + ) + .await + .expect("make comp"); + let center_geo = center_lego + .set_geometry(ctx, default_view_id, 0, 0, None, None) + .await + .expect("set center geo"); + + let mut left_lego = + create_component_for_default_schema_name_in_default_view(ctx, "Docker Image", "left lego") + .await + .expect("make comp"); + + let left_geo = left_lego + .set_geometry(ctx, default_view_id, -500, 0, None, None) + .await + .expect("set geo for left lego"); + + let mut right_lego = + create_component_for_default_schema_name_in_default_view(ctx, "Docker Image", "right lego") + .await + .expect("make comp"); + + let right_geo = right_lego + .set_geometry(ctx, default_view_id, 500, 0, None, None) + .await + .expect("set geo for left lego"); + + let mut top_lego = + create_component_for_default_schema_name_in_default_view(ctx, "Docker Image", "top lego") + .await + .expect("make comp"); + + let top_geo = top_lego + .set_geometry(ctx, default_view_id, 0, -500, None, None) + .await + .expect("set geo for left lego"); + + let mut btm_lego = + create_component_for_default_schema_name_in_default_view(ctx, "Docker Image", "btm lego") + .await + .expect("make comp"); + + let btm_geo = btm_lego + .set_geometry(ctx, default_view_id, 0, 500, None, None) + .await + .expect("set geo for left lego"); + + let mut geometries = HashMap::new(); + for geo in [ + center_geo.clone(), + left_geo, + right_geo, + btm_geo.clone(), + top_geo.clone(), + ] { + geometries.insert(ComponentId::generate(), geo.into_raw()); + } + + let (origin_x, origin_y) = + dal::management::generator::calculate_top_and_center(&geometries).await; + + assert_eq!(0, origin_x); + assert_eq!(-1000, origin_y); + + Component::set_type_by_id(ctx, left_lego.id(), ComponentType::ConfigurationFrameDown) + .await + .expect("set type"); + Component::set_type_by_id(ctx, right_lego.id(), ComponentType::ConfigurationFrameDown) + .await + .expect("set type"); + let left_geo = left_lego + .set_geometry(ctx, default_view_id, -500, 0, Some(750), Some(750)) + .await + .expect("set geo for left again"); + let right_geo = right_lego + .set_geometry(ctx, default_view_id, 500, 0, Some(750), Some(750)) + .await + .expect("set geo for left again"); + + let mut geometries = HashMap::new(); + for geo in [center_geo, left_geo, right_geo, btm_geo, top_geo] { + geometries.insert(ComponentId::generate(), geo.into_raw()); + } + + let (origin_x, origin_y) = + dal::management::generator::calculate_top_and_center(&geometries).await; + + assert_eq!(-50, origin_x); + assert_eq!(-1000, origin_y); +} diff --git a/lib/sdf-server/src/service/v2/management.rs b/lib/sdf-server/src/service/v2/management.rs index 363b8af58a..9aeed4730f 100644 --- a/lib/sdf-server/src/service/v2/management.rs +++ b/lib/sdf-server/src/service/v2/management.rs @@ -11,11 +11,14 @@ use axum::{ }; use dal::{ diagram::view::ViewId, + func::authoring::FuncAuthoringError, management::{ prototype::{ManagementPrototype, ManagementPrototypeError, ManagementPrototypeId}, ManagementError, ManagementFuncReturn, ManagementOperator, }, - ChangeSet, ChangeSetError, ChangeSetId, ComponentId, TransactionsError, WorkspacePk, + schema::variant::authoring::VariantAuthoringError, + ChangeSet, ChangeSetError, ChangeSetId, ComponentId, FuncId, SchemaVariantError, + TransactionsError, WorkspacePk, WsEventError, }; use serde::{Deserialize, Serialize}; use si_layer_cache::LayerDbError; @@ -30,6 +33,7 @@ use crate::{ use super::func::FuncAPIError; +mod generate_template; mod history; mod latest; @@ -49,6 +53,10 @@ pub enum ManagementApiError { ChangeSet(#[from] ChangeSetError), #[error("func api error: {0}")] FuncAPI(#[from] FuncAPIError), + #[error("func authoring error: {0}")] + FuncAuthoring(#[from] FuncAuthoringError), + #[error("generated mgmt func {0} has no prototype")] + FuncMissingPrototype(FuncId), #[error("hyper error: {0}")] Http(#[from] axum::http::Error), #[error("layer db error: {0}")] @@ -61,10 +69,16 @@ pub enum ManagementApiError { ManagementPrototype(#[from] ManagementPrototypeError), #[error("management prototype execution failure: {0}")] ManagementPrototypeExecutionFailure(ManagementPrototypeId), + #[error("schema variant error: {0}")] + SchemaVariant(#[from] SchemaVariantError), #[error("serde json error: {0}")] SerdeJson(#[from] serde_json::Error), #[error("transactions error: {0}")] Transactions(#[from] TransactionsError), + #[error("variant authoring error: {0}")] + VariantAuthoring(#[from] VariantAuthoringError), + #[error("ws event error: {0}")] + WsEvent(#[from] WsEventError), } impl IntoResponse for ManagementApiError { @@ -156,4 +170,8 @@ pub fn v2_routes() -> Router { get(latest::latest), ) .route("/history", get(history::history)) + .route( + "/generate_template/:viewId", + post(generate_template::generate_template), + ) } diff --git a/lib/sdf-server/src/service/v2/management/generate_template.rs b/lib/sdf-server/src/service/v2/management/generate_template.rs new file mode 100644 index 0000000000..26ff5346d1 --- /dev/null +++ b/lib/sdf-server/src/service/v2/management/generate_template.rs @@ -0,0 +1,105 @@ +use crate::service::force_change_set_response::ForceChangeSetResponse; +use axum::{ + extract::{Host, OriginalUri, Path}, + Json, +}; +use dal::{ + diagram::view::ViewId, func::authoring::FuncAuthoringClient, + management::prototype::ManagementPrototype, schema::variant::authoring::VariantAuthoringClient, + ChangeSet, ChangeSetId, ComponentId, WorkspacePk, WsEvent, +}; +use serde::{Deserialize, Serialize}; + +use crate::extract::{AccessBuilder, HandlerContext, PosthogClient}; + +use super::{ManagementApiError, ManagementApiResult}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GenerateTemplateRequest { + component_ids: Vec, + asset_name: String, + category: String, + color: String, +} + +pub async fn generate_template( + HandlerContext(builder): HandlerContext, + AccessBuilder(access_builder): AccessBuilder, + PosthogClient(_posthog_client): PosthogClient, + OriginalUri(_original_uri): OriginalUri, + Host(_host_name): Host, + Path((_workspace_pk, change_set_id, view_id)): Path<(WorkspacePk, ChangeSetId, ViewId)>, + Json(request): Json, +) -> ManagementApiResult> { + let mut ctx = builder + .build(access_builder.build(change_set_id.into())) + .await?; + + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; + + let new_variant = VariantAuthoringClient::create_schema_and_variant( + &ctx, + request.asset_name.to_owned(), + None, + None, + request.category, + request.color, + ) + .await?; + + let schema_id = new_variant.schema_id(&ctx).await?; + + let func = FuncAuthoringClient::create_new_management_func( + &ctx, + Some(request.asset_name.to_owned()), + new_variant.id(), + ) + .await?; + + let prototype_id = ManagementPrototype::list_ids_for_func_id(&ctx, func.id) + .await? + .pop() + .ok_or(ManagementApiError::FuncMissingPrototype(func.id))?; + + let (create_operations, managed_schemas) = + dal::management::generator::generate_template(&ctx, view_id, &request.component_ids) + .await?; + + let return_value = serde_json::json!({ + "status": "ok", + "message": format!("created {}", request.asset_name), + "ops": { + "create": create_operations, + } + }); + + let return_value_string = serde_json::to_string_pretty(&return_value)?; + + let code = format!( + r#"async function main({{ thisComponent, components }}: Input): Promise {{ + return {}; +}} +"#, + return_value_string + ); + + FuncAuthoringClient::save_code(&ctx, func.id, code).await?; + + let prototype = ManagementPrototype::get_by_id(&ctx, prototype_id) + .await? + .ok_or(ManagementApiError::FuncMissingPrototype(func.id))?; + + prototype + .set_managed_schemas(&ctx, Some(managed_schemas)) + .await?; + + WsEvent::schema_variant_created(&ctx, schema_id, new_variant.clone()) + .await? + .publish_on_commit(&ctx) + .await?; + + ctx.commit().await?; + + Ok(ForceChangeSetResponse::empty(force_change_set_id)) +}