diff --git a/app/web/src/components/AssetDetailsPanel.vue b/app/web/src/components/AssetDetailsPanel.vue
index 7790a021fd..617ee254a9 100644
--- a/app/web/src/components/AssetDetailsPanel.vue
+++ b/app/web/src/components/AssetDetailsPanel.vue
@@ -9,47 +9,56 @@
+
+
cloneAssetModalRef?.modal?.open()"
/>
{{ warning.message }}
@@ -64,59 +73,59 @@
-
+
@@ -146,10 +155,10 @@
{{
@@ -177,6 +186,7 @@ import { FuncKind } from "@/api/sdf/dal/func";
import { useAssetStore } from "@/store/asset.store";
import { FuncId } from "@/store/func/funcs.store";
import { ComponentType } from "@/api/sdf/dal/diagram";
+import { useFeatureFlagsStore } from "@/store/feature_flags.store";
import ColorPicker from "./ColorPicker.vue";
import AssetFuncAttachModal from "./AssetFuncAttachModal.vue";
import AssetNameModal from "./AssetNameModal.vue";
@@ -244,6 +254,14 @@ const executeAsset = async () => {
}
};
+const unlock = async () => {
+ if (assetStore.selectedAsset?.defaultSchemaVariantId) {
+ await assetStore.CREATE_UNLOCKED_COPY(
+ assetStore.selectedAsset?.defaultSchemaVariantId,
+ );
+ }
+};
+
const closeHandler = () => {
assetStore.executeAssetTaskId = undefined;
};
diff --git a/app/web/src/store/asset.store.ts b/app/web/src/store/asset.store.ts
index 4191783a28..efdc1e32ed 100644
--- a/app/web/src/store/asset.store.ts
+++ b/app/web/src/store/asset.store.ts
@@ -11,6 +11,7 @@ import router from "@/router";
import { PropKind } from "@/api/sdf/dal/prop";
import { ComponentType } from "@/api/sdf/dal/diagram";
import { nonNullable } from "@/utils/typescriptLinter";
+import { SchemaVariantId } from "@/api/sdf/dal/schema";
import { useChangeSetsStore } from "./change_sets.store";
import { useRealtimeStore } from "./realtime/realtime.store";
import {
@@ -464,6 +465,25 @@ export const useAssetStore = () => {
},
});
},
+
+ async CREATE_UNLOCKED_COPY(id: SchemaVariantId) {
+ if (changeSetsStore.creatingChangeSet)
+ throw new Error("race, wait until the change set is created");
+ if (changeSetsStore.headSelected)
+ changeSetsStore.creatingChangeSet = true;
+
+ this.detachmentWarnings = [];
+
+ return new ApiRequest({
+ method: "post",
+ url: "/variant/create_unlocked_copy",
+ keyRequestStatusBy: id,
+ params: {
+ ...visibility,
+ id,
+ },
+ });
+ },
},
async onActivated() {
await this.LOAD_ASSET_LIST();
diff --git a/app/web/src/store/feature_flags.store.ts b/app/web/src/store/feature_flags.store.ts
index 54ff68f09e..e2565d9d52 100644
--- a/app/web/src/store/feature_flags.store.ts
+++ b/app/web/src/store/feature_flags.store.ts
@@ -7,6 +7,7 @@ import { posthog } from "@/utils/posthog";
const FLAG_MAPPING = {
// STORE_FLAG_NAME: "posthogFlagName",
MODULES_TAB: "modules_tab",
+ IMMUTABLE_SCHEMA_VARIANTS: "immutable_schema_variants",
};
type FeatureFlags = keyof typeof FLAG_MAPPING;
diff --git a/lib/dal/src/attribute/prototype.rs b/lib/dal/src/attribute/prototype.rs
index cfd25ebeb9..be10473850 100644
--- a/lib/dal/src/attribute/prototype.rs
+++ b/lib/dal/src/attribute/prototype.rs
@@ -657,6 +657,27 @@ impl AttributePrototype {
}
Ok(sources)
}
+
+ pub async fn list_arguments_for_id(
+ ctx: &DalContext,
+ ap_id: AttributePrototypeId,
+ ) -> AttributePrototypeResult> {
+ let mut apas = vec![];
+
+ let snap = ctx.workspace_snapshot()?;
+
+ for idx in snap
+ .outgoing_targets_for_edge_weight_kind(
+ ap_id,
+ EdgeWeightKindDiscriminants::PrototypeArgument,
+ )
+ .await?
+ {
+ apas.push(snap.get_node_weight(idx).await?.id().into())
+ }
+
+ Ok(apas)
+ }
}
#[remain::sorted]
diff --git a/lib/dal/src/attribute/prototype/argument.rs b/lib/dal/src/attribute/prototype/argument.rs
index 3a37cc4df2..0c1e5ea9ec 100644
--- a/lib/dal/src/attribute/prototype/argument.rs
+++ b/lib/dal/src/attribute/prototype/argument.rs
@@ -74,6 +74,8 @@ pub enum AttributePrototypeArgumentError {
NodeWeight(#[from] NodeWeightError),
#[error("no targets for prototype argument: {0}")]
NoTargets(AttributePrototypeArgumentId),
+ #[error("prototype argument not found for attribute prototype {0} and func arg {1}")]
+ NotFoundForApAndFuncArg(AttributePrototypeId, FuncArgumentId),
#[error("serde json error: {0}")]
Serde(#[from] serde_json::Error),
#[error("transactions error: {0}")]
@@ -322,6 +324,27 @@ impl AttributePrototypeArgument {
Err(AttributePrototypeArgumentError::MissingFuncArgument(apa_id))
}
+ pub async fn find_by_func_argument_id_and_attribute_prototype_id(
+ ctx: &DalContext,
+ func_argument_id: FuncArgumentId,
+ ap_id: AttributePrototypeId,
+ ) -> AttributePrototypeArgumentResult {
+ // AP --> APA --> Func Arg
+
+ for apa_id in AttributePrototype::list_arguments_for_id(ctx, ap_id).await? {
+ let this_func_arg_id = Self::func_argument_id_by_id(ctx, apa_id).await?;
+
+ if this_func_arg_id == func_argument_id {
+ return Ok(apa_id);
+ }
+ }
+
+ Err(AttributePrototypeArgumentError::NotFoundForApAndFuncArg(
+ ap_id,
+ func_argument_id,
+ ))
+ }
+
pub async fn value_source(
&self,
ctx: &DalContext,
diff --git a/lib/dal/src/diagram.rs b/lib/dal/src/diagram.rs
index aaefbaeb2a..7bd8b46f17 100644
--- a/lib/dal/src/diagram.rs
+++ b/lib/dal/src/diagram.rs
@@ -227,7 +227,7 @@ impl SummaryDiagramComponent {
schema_name: schema.name().to_owned(),
schema_id: schema.id(),
schema_variant_id: schema_variant.id(),
- schema_variant_name: schema_variant.name().to_owned(),
+ schema_variant_name: schema_variant.version().to_owned(),
schema_category: schema_variant.category().to_owned(),
display_name: component.name(ctx).await?,
position,
diff --git a/lib/dal/src/history_event.rs b/lib/dal/src/history_event.rs
index f979f57090..fcc520a0dc 100644
--- a/lib/dal/src/history_event.rs
+++ b/lib/dal/src/history_event.rs
@@ -43,6 +43,7 @@ impl HistoryActor {
HistoryActor::SystemInit => "unknown-backend".to_string(),
}
}
+
pub async fn email(&self, ctx: &DalContext) -> HistoryEventResult {
Ok(match self {
HistoryActor::SystemInit => "sally@systeminit.com".to_string(),
diff --git a/lib/dal/src/layer_db_types/content_types.rs b/lib/dal/src/layer_db_types/content_types.rs
index d7ed2793d8..6adaf3c07a 100644
--- a/lib/dal/src/layer_db_types/content_types.rs
+++ b/lib/dal/src/layer_db_types/content_types.rs
@@ -2,17 +2,28 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use si_events::{CasValue, ContentHash};
use strum::EnumDiscriminants;
+use thiserror::Error;
use crate::action::prototype::ActionKind;
use crate::validation::ValidationStatus;
use crate::{
action::ActionCompletionStatus, func::argument::FuncArgumentKind, prop::WidgetOptions,
property_editor::schema::WidgetKind, socket::connection_annotation::ConnectionAnnotation,
- ActionPrototypeId, ComponentId, ComponentType, FuncBackendKind, FuncBackendResponseType,
- FuncId, PropId, PropKind, SocketArity, SocketKind, Timestamp, UserPk,
+ ActionPrototypeId, ComponentId, ComponentType, DalContext, FuncBackendKind,
+ FuncBackendResponseType, FuncId, PropId, PropKind, SchemaVariant, SchemaVariantId, SocketArity,
+ SocketKind, Timestamp, UserPk,
};
-/// This type gathers up all the kinds of things we will store in the
+#[remain::sorted]
+#[derive(Error, Debug)]
+pub enum ContentTypeError {
+ #[error("error extracting schema variant content : {0}")]
+ SchemaVariantContent(String),
+}
+
+pub type ContentTypeResult = Result;
+
+/// This type gathers all the kinds of things we will store in the
/// content-store portion of the layered database. Anything we want to read or
/// write from there should be added here. Then the impl_into_content_types!
/// macro should be used to provide from/into implementations between the inner
@@ -346,6 +357,54 @@ pub struct SchemaContentV1 {
#[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)]
pub enum SchemaVariantContent {
V1(SchemaVariantContentV1),
+ V2(SchemaVariantContentV2),
+}
+
+impl SchemaVariantContent {
+ pub async fn extract(
+ self,
+ ctx: &DalContext,
+ id: SchemaVariantId,
+ ) -> ContentTypeResult {
+ // update progressively
+ let at_least_v2 = match self {
+ SchemaVariantContent::V1(v1) => {
+ let display_name = if let Some(display_name) = v1.display_name {
+ display_name
+ } else {
+ let schema = SchemaVariant::schema_for_schema_variant_id(ctx, id)
+ .await
+ .map_err(|e| ContentTypeError::SchemaVariantContent(e.to_string()))?;
+ schema.name
+ };
+
+ SchemaVariantContent::V2(SchemaVariantContentV2 {
+ timestamp: v1.timestamp,
+ ui_hidden: v1.ui_hidden,
+ version: v1.name.to_owned(),
+ display_name,
+ category: v1.category,
+ color: v1.color,
+ component_type: v1.component_type,
+ link: v1.link,
+ description: v1.description,
+ asset_func_id: v1.asset_func_id,
+ finalized_once: v1.finalized_once,
+ is_builtin: v1.is_builtin,
+ is_locked: true,
+ })
+ }
+ later => later,
+ };
+
+ // extract latest
+ let latest = match at_least_v2 {
+ SchemaVariantContent::V2(v2) => v2,
+ _ => unreachable!(),
+ };
+
+ Ok(latest)
+ }
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
@@ -364,6 +423,23 @@ pub struct SchemaVariantContentV1 {
pub is_builtin: bool,
}
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
+pub struct SchemaVariantContentV2 {
+ pub timestamp: Timestamp,
+ pub ui_hidden: bool,
+ pub version: String,
+ pub display_name: String,
+ pub category: String,
+ pub color: String,
+ pub component_type: ComponentType,
+ pub link: Option,
+ pub description: Option,
+ pub asset_func_id: Option,
+ pub finalized_once: bool,
+ pub is_builtin: bool,
+ pub is_locked: bool,
+}
+
#[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)]
pub enum SecretContent {
V1(SecretContentV1),
diff --git a/lib/dal/src/pkg/export.rs b/lib/dal/src/pkg/export.rs
index cc7b5b378d..754d1e17ca 100644
--- a/lib/dal/src/pkg/export.rs
+++ b/lib/dal/src/pkg/export.rs
@@ -170,12 +170,12 @@ impl PkgExporter {
variant_is_builtin: bool,
) -> PkgResult {
let mut variant_spec_builder = SchemaVariantSpec::builder();
- variant_spec_builder.name(variant.name());
+ variant_spec_builder.name(variant.version());
variant_spec_builder.is_builtin(variant_is_builtin);
variant_spec_builder.unique_id(variant.id().to_string());
let mut data_builder = SchemaVariantSpecData::builder();
- data_builder.name(variant.name());
+ data_builder.name(variant.version());
data_builder.color(variant.get_color(ctx).await?);
if let Some(link) = variant.link() {
diff --git a/lib/dal/src/pkg/import.rs b/lib/dal/src/pkg/import.rs
index 825c3b752f..92337e881a 100644
--- a/lib/dal/src/pkg/import.rs
+++ b/lib/dal/src/pkg/import.rs
@@ -468,7 +468,7 @@ async fn import_schema(
for variant_spec in &schema_spec.variants()? {
let variant = import_schema_variant(
ctx,
- &mut schema,
+ &schema,
schema_spec.clone(),
variant_spec,
installed_module.clone(),
@@ -478,7 +478,10 @@ async fn import_schema(
.await?;
if let Some(variant) = variant {
- installed_schema_variant_ids.push(variant.id());
+ let variant_id = variant.id();
+ variant.lock(ctx).await?;
+
+ installed_schema_variant_ids.push(variant_id);
set_default_schema_variant_id(
ctx,
@@ -488,7 +491,7 @@ async fn import_schema(
.as_ref()
.and_then(|data| data.default_schema_variant()),
variant_spec.unique_id(),
- variant.id(),
+ variant_id,
)
.await?;
}
@@ -943,7 +946,7 @@ pub async fn import_only_new_funcs(
#[allow(clippy::too_many_arguments)]
pub(crate) async fn import_schema_variant(
ctx: &DalContext,
- schema: &mut Schema,
+ schema: &Schema,
schema_spec: SiPkgSchema<'_>,
variant_spec: &SiPkgSchemaVariant<'_>,
installed_module: Option,
@@ -957,7 +960,7 @@ pub(crate) async fn import_schema_variant(
installed_pkg.list_associated_schema_variants(ctx).await?;
let mut maybe_matching_schema_variant: Vec = associated_schema_variants
.into_iter()
- .filter(|s| s.name() == schema_spec.name())
+ .filter(|s| s.version() == schema_spec.name())
.collect();
if let Some(matching_schema_variant) = maybe_matching_schema_variant.pop() {
existing_schema_variant = Some(matching_schema_variant);
@@ -968,10 +971,11 @@ pub(crate) async fn import_schema_variant(
existing_schema_variant = Some(existing_sv);
}
+ // TODO `created` is always true. if that's correct, none of the sv vars should be optional, including the return type
let (variant, created) = match existing_schema_variant {
None => {
let spec = schema_spec.to_spec().await?;
- let schema_name = &spec.name.clone();
+ let schema_name = spec.name.clone();
let metadata = SchemaVariantJson::metadata_from_spec(spec)?;
let mut asset_func_id: Option = None;
@@ -988,29 +992,24 @@ pub(crate) async fn import_schema_variant(
asset_func_id = Some(asset_func.id)
}
}
- let display_name = match metadata.display_name {
- Some(name) => name,
- None => schema_name.into(),
- };
- info!(%display_name, "display_name");
- (
- SchemaVariant::new(
- ctx,
- schema.id(),
- variant_spec.name(),
- display_name,
- metadata.category,
- metadata.color,
- metadata.component_type,
- metadata.link,
- metadata.description,
- asset_func_id,
- variant_spec.is_builtin(),
- )
- .await?
- .0,
- true,
+
+ let variant = SchemaVariant::new(
+ ctx,
+ schema.id(),
+ variant_spec.name(),
+ metadata.display_name.unwrap_or(schema_name),
+ metadata.category,
+ metadata.color,
+ metadata.component_type,
+ metadata.link,
+ metadata.description,
+ asset_func_id,
+ variant_spec.is_builtin(),
)
+ .await?
+ .0;
+
+ (variant, true)
}
Some(installed_variant) => (installed_variant, true),
};
diff --git a/lib/dal/src/prop.rs b/lib/dal/src/prop.rs
index d1599eb01d..713afbcb7c 100644
--- a/lib/dal/src/prop.rs
+++ b/lib/dal/src/prop.rs
@@ -625,7 +625,7 @@ impl Prop {
let mut work_queue = VecDeque::from([prop_id]);
while let Some(prop_id) = work_queue.pop_front() {
- if let Some(prop_id) = Prop::parent_prop_id_by_id(ctx, prop_id).await? {
+ if let Some(prop_id) = Self::parent_prop_id_by_id(ctx, prop_id).await? {
let workspace_snapshot = ctx.workspace_snapshot()?;
let node_idx = workspace_snapshot.get_node_index_by_id(prop_id).await?;
@@ -690,7 +690,7 @@ impl Prop {
// NOTE(nick,jacob,zack): if we had a v2, then there would be migration logic here.
let PropContent::V1(inner) = content;
- Ok(Prop::assemble(node_weight, inner))
+ Ok(Self::assemble(node_weight, inner))
}
pub async fn element_prop_id(&self, ctx: &DalContext) -> PropResult {
@@ -917,7 +917,7 @@ impl Prop {
ctx: &DalContext,
prop_id: PropId,
) -> PropResult