diff --git a/app/web/src/components/AssetDetailsPanel.vue b/app/web/src/components/AssetDetailsPanel.vue index 6b69c42cf1..9f183ea489 100644 --- a/app/web/src/components/AssetDetailsPanel.vue +++ b/app/web/src/components/AssetDetailsPanel.vue @@ -25,9 +25,15 @@ tone="neutral" icon="clipboard-copy" size="md" - @click="cloneAsset" + @click="() => cloneAssetModalRef?.modal?.open()" /> + >(); const openAttachModal = (warning: { kind?: FuncKind; funcId?: FuncId }) => { if (!warning.kind) return; @@ -232,10 +240,11 @@ const closeHandler = () => { assetStore.executeAssetTaskId = undefined; }; -const cloneAsset = async () => { +const cloneAsset = async (name: string) => { if (editingAsset.value?.id) { - const result = await assetStore.CLONE_ASSET(editingAsset.value.id); + const result = await assetStore.CLONE_ASSET(editingAsset.value.id, name); if (result.result.success) { + cloneAssetModalRef.value?.modal?.close(); await assetStore.setAssetSelection(result.result.data.id); } } diff --git a/app/web/src/components/AssetListPanel.vue b/app/web/src/components/AssetListPanel.vue index 5b22641ae1..22a356cfb3 100644 --- a/app/web/src/components/AssetListPanel.vue +++ b/app/web/src/components/AssetListPanel.vue @@ -24,7 +24,7 @@ tooltip="New Asset" tooltipPlacement="top" loadingTooltip="Creating Asset..." - @click="newAsset" + @click="() => newAssetModalRef?.modal?.open()" /> + >(); const exportSuccessModalRef = ref>(); +const newAssetModalRef = ref>(); const contributeLoadingTexts = [ "Engaging Photon Torpedos...", @@ -219,10 +227,17 @@ const categoryColor = (category: string) => { return "#000"; }; -const newAsset = async () => { - const result = await assetStore.CREATE_ASSET(assetStore.createNewAsset()); +const newAsset = async (newAssetName: string) => { + const result = await assetStore.CREATE_ASSET( + assetStore.createNewAsset(newAssetName), + ); if (result.result.success) { assetStore.setAssetSelection(result.result.data.id); + newAssetModalRef.value?.modal?.close(); + } else if (result.result.statusCode === 409) { + if (newAssetModalRef.value) { + newAssetModalRef.value.setError("That name is already in use"); + } } }; diff --git a/app/web/src/components/AssetNameModal.vue b/app/web/src/components/AssetNameModal.vue new file mode 100644 index 0000000000..38f39331b6 --- /dev/null +++ b/app/web/src/components/AssetNameModal.vue @@ -0,0 +1,44 @@ + + + diff --git a/app/web/src/store/asset.store.ts b/app/web/src/store/asset.store.ts index 63769c0718..71bc0bf468 100644 --- a/app/web/src/store/asset.store.ts +++ b/app/web/src/store/asset.store.ts @@ -103,7 +103,7 @@ export type AssetCreateRequest = Omit< AssetSaveRequest, "id" | "definition" | "variantExists" >; -export type AssetCloneRequest = Visibility & { id: AssetId }; +export type AssetCloneRequest = Visibility & { id: AssetId; name: string }; export const assetDisplayName = (asset: Asset | AssetListEntry) => (asset.displayName ?? "").length === 0 ? asset.name : asset.displayName; @@ -298,11 +298,13 @@ export const useAssetStore = () => { ])}`; }, - createNewAsset(): Asset { + createNewAsset(name?: string): Asset { + name = name || `new asset ${Math.floor(Math.random() * 10000)}`; return { id: nilId(), defaultSchemaVariantId: "", - name: `new asset ${Math.floor(Math.random() * 10000)}`, + name, + displayName: name, code: "", color: this.generateMockColor(), description: "", @@ -339,7 +341,7 @@ export const useAssetStore = () => { }); }, - async CLONE_ASSET(assetId: AssetId) { + async CLONE_ASSET(assetId: AssetId, name: string) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) @@ -355,6 +357,7 @@ export const useAssetStore = () => { params: { ...visibility, id: assetId, + name, }, }); }, diff --git a/lib/dal/src/schema.rs b/lib/dal/src/schema.rs index a042bd8ce3..91b172c026 100644 --- a/lib/dal/src/schema.rs +++ b/lib/dal/src/schema.rs @@ -473,4 +473,8 @@ impl Schema { let funcs = Func::list_from_ids(ctx, func_ids.as_slice()).await?; Ok(funcs) } + + pub async fn is_name_taken(ctx: &DalContext, name: &String) -> SchemaResult { + Ok(Self::list(ctx).await?.iter().any(|s| s.name.eq(name))) + } } diff --git a/lib/dal/src/schema/variant/authoring.rs b/lib/dal/src/schema/variant/authoring.rs index cd560c7f72..1f94d73402 100644 --- a/lib/dal/src/schema/variant/authoring.rs +++ b/lib/dal/src/schema/variant/authoring.rs @@ -20,9 +20,8 @@ use crate::pkg::import::import_only_new_funcs; use crate::pkg::{import_pkg_from_pkg, PkgError}; use crate::schema::variant::{SchemaVariantJson, SchemaVariantMetadataJson}; use crate::{ - generate_unique_id, pkg, ComponentType, DalContext, Func, FuncBackendKind, - FuncBackendResponseType, FuncError, FuncId, Schema, SchemaError, SchemaVariant, - SchemaVariantError, SchemaVariantId, + pkg, ComponentType, DalContext, Func, FuncBackendKind, FuncBackendResponseType, FuncError, + FuncId, Schema, SchemaError, SchemaVariant, SchemaVariantError, SchemaVariantId, }; #[allow(missing_docs)] @@ -82,7 +81,7 @@ impl VariantAuthoringClient { /// Creates a [`SchemaVariant`] and returns the [result](SchemaVariant). #[instrument(name = "variant.authoring.create_variant", level = "info", skip_all)] #[allow(clippy::too_many_arguments)] - pub async fn create_variant( + pub async fn create_schema_and_variant( ctx: &DalContext, name: impl Into, display_name: Option, @@ -112,7 +111,7 @@ impl VariantAuthoringClient { let definition = execute_asset_func(ctx, &asset_func).await?; let metadata = SchemaVariantMetadataJson { - name, + schema_name: name, menu_name: display_name.clone(), category: category.into(), color: color.into(), @@ -159,23 +158,22 @@ impl VariantAuthoringClient { pub async fn clone_variant( ctx: &DalContext, schema_variant_id: SchemaVariantId, + name: String, ) -> VariantAuthoringResult<(SchemaVariant, Schema)> { - println!("clone variant"); let variant = SchemaVariant::get_by_id(ctx, schema_variant_id).await?; let schema = variant.schema(ctx).await?; - let new_name = format!("{} Clone {}", schema.name(), generate_unique_id(4)); - let menu_name = variant.display_name().map(|mn| format!("{mn} Clone")); + let display_name = variant.display_name().map(|dn| format!("{dn} Clone")); if let Some(asset_func_id) = variant.asset_func_id() { let old_func = Func::get_by_id_or_error(ctx, asset_func_id).await?; - let cloned_func = old_func.duplicate(ctx, new_name.clone()).await?; + let cloned_func = old_func.duplicate(ctx, name.clone()).await?; let cloned_func_spec = build_asset_func_spec(&cloned_func)?; let definition = execute_asset_func(ctx, &cloned_func).await?; let metadata = SchemaVariantMetadataJson { - name: new_name.clone(), - menu_name: menu_name.clone(), + schema_name: name, + menu_name: display_name.clone(), category: variant.category().to_string(), color: variant.get_color(ctx).await?, component_type: variant.component_type(), @@ -331,7 +329,7 @@ impl VariantAuthoringClient { let asset_func_spec = build_asset_func_spec(&asset_func)?; let definition = execute_asset_func(ctx, &asset_func).await?; let metadata = SchemaVariantMetadataJson { - name: name.clone(), + schema_name: name.clone(), menu_name: menu_name.clone(), category: category.clone(), color: color.clone(), @@ -350,10 +348,10 @@ impl VariantAuthoringClient { ) .await?; - let schema_spec = metadata.to_spec(new_variant_spec)?; + let schema_spec = metadata.to_schema_spec(new_variant_spec)?; //TODO @stack72 - figure out how we get the current user in this! let pkg_spec = PkgSpec::builder() - .name(&metadata.name) + .name(&metadata.schema_name) .created_by("sally@systeminit.com") .funcs(variant_funcs.clone()) .func(asset_func_spec) @@ -475,7 +473,7 @@ impl VariantAuthoringClient { let definition = execute_asset_func(ctx, &new_asset_func).await?; let metadata = SchemaVariantMetadataJson { - name: name.clone(), + schema_name: name.clone(), menu_name: menu_name.clone(), category: category.into(), color: color.into(), @@ -494,11 +492,11 @@ impl VariantAuthoringClient { ) .await?; - let schema_spec = metadata.to_spec(new_variant_spec)?; + let schema_spec = metadata.to_schema_spec(new_variant_spec)?; //TODO @stack72 - figure out how we get the current user in this! let pkg_spec = PkgSpec::builder() - .name(&metadata.name) + .name(&metadata.schema_name) .created_by("sally@systeminit.com") .funcs(variant_funcs.clone()) .func(asset_func_spec) @@ -746,9 +744,9 @@ fn build_pkg_spec_for_variant( &identity_func_spec.unique_id, &asset_func_spec.unique_id, )?; - let schema_spec = metadata.to_spec(variant_spec)?; + let schema_spec = metadata.to_schema_spec(variant_spec)?; Ok(PkgSpec::builder() - .name(metadata.clone().name) + .name(metadata.clone().schema_name) .created_by(user_email) .func(identity_func_spec) .func(asset_func_spec.clone()) diff --git a/lib/dal/src/schema/variant/json.rs b/lib/dal/src/schema/variant/json.rs index 56e011e58c..b56629e33a 100644 --- a/lib/dal/src/schema/variant/json.rs +++ b/lib/dal/src/schema/variant/json.rs @@ -16,7 +16,7 @@ use crate::{ComponentType, PropKind, SchemaVariantError, SocketArity}; pub struct SchemaVariantMetadataJson { /// Name for this variant. Actually, this is the name for this [`Schema`](crate::Schema), we're /// punting on the issue of multiple variants for the moment. - pub name: String, + pub schema_name: String, /// Override for the UI name for this schema #[serde(alias = "menu_name")] pub menu_name: Option, @@ -31,11 +31,11 @@ pub struct SchemaVariantMetadataJson { } impl SchemaVariantMetadataJson { - pub fn to_spec(&self, variant: SchemaVariantSpec) -> SchemaVariantResult { + pub fn to_schema_spec(&self, variant: SchemaVariantSpec) -> SchemaVariantResult { let mut builder = SchemaSpec::builder(); - builder.name(&self.name); + builder.name(&self.schema_name); let mut data_builder = SchemaSpecData::builder(); - data_builder.name(&self.name); + data_builder.name(&self.schema_name); data_builder.category(&self.category); if let Some(menu_name) = &self.menu_name { data_builder.category_name(menu_name.as_str()); @@ -168,7 +168,7 @@ impl SchemaVariantJson { }); let metadata = SchemaVariantMetadataJson { - name: schema_spec.name, + schema_name: schema_spec.name, menu_name: schema_data.category_name, category: schema_data.category, color: variant_spec_data diff --git a/lib/dal/tests/integration_test/component/upgrade.rs b/lib/dal/tests/integration_test/component/upgrade.rs index de22730ae2..0ff48c0532 100644 --- a/lib/dal/tests/integration_test/component/upgrade.rs +++ b/lib/dal/tests/integration_test/component/upgrade.rs @@ -21,7 +21,7 @@ async fn upgrade_component(ctx: &mut DalContext) { let link = None; let category = "Integration Tests".to_string(); let color = "#00b0b0".to_string(); - let variant_zero = VariantAuthoringClient::create_variant( + let variant_zero = VariantAuthoringClient::create_schema_and_variant( ctx, asset_name.clone(), display_name.clone(), diff --git a/lib/dal/tests/integration_test/func/authoring/create_func.rs b/lib/dal/tests/integration_test/func/authoring/create_func.rs index a290862cd3..03e82686fd 100644 --- a/lib/dal/tests/integration_test/func/authoring/create_func.rs +++ b/lib/dal/tests/integration_test/func/authoring/create_func.rs @@ -540,7 +540,7 @@ async fn create_qualification_and_code_gen_with_existing_component(ctx: &mut Dal let link = None; let category = "Integration Tests".to_string(); let color = "#00b0b0".to_string(); - let variant_zero = VariantAuthoringClient::create_variant( + let variant_zero = VariantAuthoringClient::create_schema_and_variant( ctx, asset_name.clone(), display_name.clone(), diff --git a/lib/dal/tests/integration_test/pkg/mod.rs b/lib/dal/tests/integration_test/pkg/mod.rs index 1dbcca401e..9a0396e724 100644 --- a/lib/dal/tests/integration_test/pkg/mod.rs +++ b/lib/dal/tests/integration_test/pkg/mod.rs @@ -14,7 +14,7 @@ async fn import_pkg_from_pkg_set_latest_default(ctx: &mut DalContext) { let link = None; let category = "Integration Tests".to_string(); let color = "#00b0b0".to_string(); - let variant = VariantAuthoringClient::create_variant( + let variant = VariantAuthoringClient::create_schema_and_variant( ctx, asset_name.clone(), display_name.clone(), diff --git a/lib/dal/tests/integration_test/schema/variant/authoring/clone_variant.rs b/lib/dal/tests/integration_test/schema/variant/authoring/clone_variant.rs index e035948f91..bf24f4bdcb 100644 --- a/lib/dal/tests/integration_test/schema/variant/authoring/clone_variant.rs +++ b/lib/dal/tests/integration_test/schema/variant/authoring/clone_variant.rs @@ -32,6 +32,7 @@ async fn clone_variant(ctx: &mut DalContext) { let (new_schema_variant, _) = VariantAuthoringClient::clone_variant( ctx, default_schema_variant.expect("unable to get the schema variant id from the option"), + existing_schema.name().to_string(), ) .await .expect("unable to clone the schema variant"); diff --git a/lib/dal/tests/integration_test/schema/variant/authoring/create_variant.rs b/lib/dal/tests/integration_test/schema/variant/authoring/create_variant.rs index f51bb78b08..b448ff4cbc 100644 --- a/lib/dal/tests/integration_test/schema/variant/authoring/create_variant.rs +++ b/lib/dal/tests/integration_test/schema/variant/authoring/create_variant.rs @@ -18,7 +18,7 @@ async fn create_variant(ctx: &mut DalContext) { let link = None; let category = "Integration Tests".to_string(); let color = "#00b0b0".to_string(); - let variant = VariantAuthoringClient::create_variant( + let variant = VariantAuthoringClient::create_schema_and_variant( ctx, asset_name.clone(), display_name.clone(), diff --git a/lib/dal/tests/integration_test/schema/variant/authoring/save_variant.rs b/lib/dal/tests/integration_test/schema/variant/authoring/save_variant.rs index c399a4b62a..a769a57fac 100644 --- a/lib/dal/tests/integration_test/schema/variant/authoring/save_variant.rs +++ b/lib/dal/tests/integration_test/schema/variant/authoring/save_variant.rs @@ -18,7 +18,7 @@ async fn save_variant(ctx: &mut DalContext) { let link = None; let category = "Integration Tests".to_string(); let color = "#00b0b0".to_string(); - let variant = VariantAuthoringClient::create_variant( + let variant = VariantAuthoringClient::create_schema_and_variant( ctx, asset_name.clone(), display_name.clone(), diff --git a/lib/dal/tests/integration_test/schema/variant/authoring/update_variant.rs b/lib/dal/tests/integration_test/schema/variant/authoring/update_variant.rs index ac39f393e6..295f007b3a 100644 --- a/lib/dal/tests/integration_test/schema/variant/authoring/update_variant.rs +++ b/lib/dal/tests/integration_test/schema/variant/authoring/update_variant.rs @@ -24,7 +24,7 @@ async fn update_variant(ctx: &mut DalContext) { let link = None; let category = "Integration Tests".to_string(); let color = "#00b0b0".to_string(); - let my_first_variant = VariantAuthoringClient::create_variant( + let my_first_variant = VariantAuthoringClient::create_schema_and_variant( ctx, asset_name.clone(), display_name.clone(), @@ -138,7 +138,7 @@ async fn update_variant(ctx: &mut DalContext) { #[test] async fn update_variant_with_new_prototypes_for_new_func(ctx: &mut DalContext) { - let first_variant = VariantAuthoringClient::create_variant( + let first_variant = VariantAuthoringClient::create_schema_and_variant( ctx, "helix", None, @@ -309,7 +309,7 @@ async fn update_variant_with_new_prototypes_for_new_func(ctx: &mut DalContext) { #[test] async fn update_variant_with_leaf_func(ctx: &mut DalContext) { - let schema_variant = VariantAuthoringClient::create_variant( + let schema_variant = VariantAuthoringClient::create_schema_and_variant( ctx, "helix", None, diff --git a/lib/dal/tests/integration_test/secret/with_schema_variant_authoring.rs b/lib/dal/tests/integration_test/secret/with_schema_variant_authoring.rs index dec602c037..fd1a894c38 100644 --- a/lib/dal/tests/integration_test/secret/with_schema_variant_authoring.rs +++ b/lib/dal/tests/integration_test/secret/with_schema_variant_authoring.rs @@ -10,7 +10,7 @@ use dal_test::test; #[test] async fn existing_code_gen_func_using_secrets_for_new_schema_variant(ctx: &mut DalContext) { // Create a new schema variant and commit. - let schema_variant = VariantAuthoringClient::create_variant( + let schema_variant = VariantAuthoringClient::create_schema_and_variant( ctx, "ergo sum", None, None, None, "bungie", "#00b0b0", ) .await diff --git a/lib/sdf-server/src/server/service/variant/clone_variant.rs b/lib/sdf-server/src/server/service/variant/clone_variant.rs index 4bbb1eaea6..429d1f8d91 100644 --- a/lib/sdf-server/src/server/service/variant/clone_variant.rs +++ b/lib/sdf-server/src/server/service/variant/clone_variant.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct CloneVariantRequest { pub id: SchemaId, + pub name: String, #[serde(flatten)] pub visibility: Visibility, } @@ -31,6 +32,12 @@ pub async fn clone_variant( ) -> SchemaVariantResult { let mut ctx = builder.build(request_ctx.build(request.visibility)).await?; + if Schema::is_name_taken(&ctx, &request.name).await? { + return Ok(axum::response::Response::builder() + .status(409) + .body("schema name already taken".to_string())?); + } + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; let schema = Schema::get_by_id(&ctx, request.id).await?; @@ -38,8 +45,12 @@ pub async fn clone_variant( SchemaVariantError::NoDefaultSchemaVariantFoundForSchema(schema.id()), )?; - let (cloned_schema_variant, schema) = - VariantAuthoringClient::clone_variant(&ctx, default_schema_variant_id).await?; + let (cloned_schema_variant, schema) = VariantAuthoringClient::clone_variant( + &ctx, + default_schema_variant_id, + request.name.clone(), + ) + .await?; track( &posthog_client, @@ -47,7 +58,7 @@ pub async fn clone_variant( &original_uri, "clone_variant", serde_json::json!({ - "variant_name": schema.name(), + "variant_name": request.name, "variant_category": cloned_schema_variant.category(), "variant_menu_name": cloned_schema_variant.display_name(), "variant_id": cloned_schema_variant.id(), diff --git a/lib/sdf-server/src/server/service/variant/create_variant.rs b/lib/sdf-server/src/server/service/variant/create_variant.rs index 04276d4619..29ee1af6c2 100644 --- a/lib/sdf-server/src/server/service/variant/create_variant.rs +++ b/lib/sdf-server/src/server/service/variant/create_variant.rs @@ -3,7 +3,7 @@ use axum::{response::IntoResponse, Json}; use serde::{Deserialize, Serialize}; use dal::schema::variant::authoring::VariantAuthoringClient; -use dal::{ChangeSet, SchemaId, Visibility, WsEvent}; +use dal::{ChangeSet, Schema, SchemaId, Visibility, WsEvent}; use crate::server::extract::{AccessBuilder, HandlerContext, PosthogClient}; use crate::server::tracking::track; @@ -26,7 +26,6 @@ pub struct CreateVariantRequest { #[serde(rename_all = "camelCase")] pub struct CreateVariantResponse { pub id: SchemaId, - pub success: bool, } pub async fn create_variant( @@ -38,9 +37,15 @@ pub async fn create_variant( ) -> SchemaVariantResult { let mut ctx = builder.build(request_ctx.build(request.visibility)).await?; + if Schema::is_name_taken(&ctx, &request.name).await? { + return Ok(axum::response::Response::builder() + .status(409) + .body("schema name already taken".to_string())?); + } + let force_change_set_id = ChangeSet::force_new(&mut ctx).await?; - let created_schema_variant = VariantAuthoringClient::create_variant( + let created_schema_variant = VariantAuthoringClient::create_schema_and_variant( &ctx, request.name.clone(), request.display_name.clone(), @@ -87,6 +92,5 @@ pub async fn create_variant( } Ok(response.body(serde_json::to_string(&CreateVariantResponse { id: schema.id(), - success: true, })?)?) } diff --git a/lib/vue-lib/src/pinia/pinia_api_tools.ts b/lib/vue-lib/src/pinia/pinia_api_tools.ts index fbd0794374..8aa4a7a65c 100644 --- a/lib/vue-lib/src/pinia/pinia_api_tools.ts +++ b/lib/vue-lib/src/pinia/pinia_api_tools.ts @@ -106,7 +106,12 @@ export class ApiRequest< // ie, checking success guarantees data is present get result(): | { success: true; data: Response } - | { success: false; err: Error; errBody?: any } { + | { + success: false; + err: Error; + errBody?: any; + statusCode?: number | undefined; + } { /* eslint-disable @typescript-eslint/no-non-null-assertion */ if (this.rawSuccess === undefined) throw new Error("You must await the request to access the result"); @@ -121,6 +126,7 @@ export class ApiRequest< // the (json) body of the failed request, if applicable ...(this.rawResponseError instanceof AxiosError && { errBody: this.rawResponseError.response?.data, + statusCode: this.rawResponseError.response?.status, }), }; }