Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit 4f948a9

Browse files
merge: #3479
3479: fix(dal): make sure you can manually override maps and arrays r=vbustamante a=vbustamante Previously, the rebaser was skipping any unordered children of ordered containers. Since an attribute value for a map has an ordering node connection, when computing its children, the rebaser was only considering those edges to those nodes as updates. Since when we do an override we’re binding an attributePrototype directly to the attribute value, without the ordering connection, the rebaser was skipping it. But this was working fine for string attribute values, for instance, since they are considered unordered containers (they’re not containers at all, but since they don’t have the ordering node that’s how we treat them). This PR also fixes an issue in which adding an ordered node to a fresh graph fails, which was forcing unit tests to be more complex than they needed to be Co-authored-by: Victor Bustamante <victor@systeminit.com>
2 parents 6034186 + 2101359 commit 4f948a9

File tree

11 files changed

+489
-416
lines changed

11 files changed

+489
-416
lines changed

app/web/src/components/AttributesPanel/AttributesPanelItem.vue

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@
308308
/>
309309
<Icon
310310
v-if="
311+
sourceOverridden &&
311312
currentValue !== null &&
312313
!propPopulatedBySocket &&
313314
!propControlledByParent
@@ -445,7 +446,7 @@
445446
v-if="
446447
propControlledByParent ||
447448
(featureFlagsStore.INDICATORS_MANUAL_FUNCTION_SOCKET &&
448-
propPopulatedBySocket &&
449+
propSetByDynamicFunc &&
449450
!editOverride)
450451
"
451452
v-tooltip="
@@ -556,8 +557,8 @@
556557
function from an ancestor prop.
557558
</template>
558559
<template v-else>
559-
Editing the prop "{{ propName }}" directly will override the socket
560-
which is currently setting its value.
560+
Editing the prop "{{ propName }}" directly will override the value
561+
that is set by a dynamic function.
561562
</template>
562563
</div>
563564
<div class="flex gap-sm">
@@ -751,7 +752,7 @@ const propPopulatedBySocket = computed(
751752
const propHasSocket = computed(
752753
() => props.attributeDef.value?.canBeSetBySocket,
753754
);
754-
const propSetByFunc = computed(
755+
const propSetByDynamicFunc = computed(
755756
() =>
756757
props.attributeDef.value?.isControlledByDynamicFunc &&
757758
!propHasSocket.value &&
@@ -762,18 +763,18 @@ const propManual = computed(
762763
!(
763764
propPopulatedBySocket.value ||
764765
propHasSocket.value ||
765-
propSetByFunc.value
766+
propSetByDynamicFunc.value
766767
),
767768
);
768769
const propSource = computed<SourceKind>(() => {
769770
if (propHasSocket.value || propPopulatedBySocket.value) return "via socket";
770-
else if (propSetByFunc.value) return "via attribute func";
771+
else if (propSetByDynamicFunc.value) return "via attribute func";
771772
else return "manually";
772773
});
773774
774775
const sourceIcon = computed(() => {
775776
if (propPopulatedBySocket.value) return "circle-full";
776-
else if (propSetByFunc.value) return "func";
777+
else if (propSetByDynamicFunc.value) return "func";
777778
else if (propHasSocket.value) return "circle-empty";
778779
else return "cursor";
779780
});
@@ -784,7 +785,7 @@ const sourceTooltip = computed(() => {
784785
if (sourceOverridden.value) {
785786
if (propPopulatedBySocket.value) {
786787
return `${propName.value} has been overriden to be set via a populated socket`;
787-
} else if (propSetByFunc.value) {
788+
} else if (propSetByDynamicFunc.value) {
788789
return `${propName.value} has been overriden to be set by a dynamic function`;
789790
} else if (propHasSocket.value) {
790791
return `${propName.value} has been overriden to be set via an empty socket`;
@@ -793,7 +794,7 @@ const sourceTooltip = computed(() => {
793794
} else {
794795
if (propPopulatedBySocket.value) {
795796
return `${propName.value} is set via a populated socket`;
796-
} else if (propSetByFunc.value) {
797+
} else if (propSetByDynamicFunc.value) {
797798
return `${propName.value} is set by a dynamic function`;
798799
} else if (propHasSocket.value) {
799800
return `${propName.value} is set via an empty socket`;
@@ -876,17 +877,6 @@ function unsetHandler() {
876877
newValueBoolean.value = false;
877878
newValueString.value = "";
878879
879-
// TODO(Wendy) - OLD CODE, REMOVE ONCE RESET_PROPERTY_VALUE IS WIRED UP TO THE BACKEND
880-
// attributesStore.UPDATE_PROPERTY_VALUE({
881-
// update: {
882-
// attributeValueId: props.attributeDef.valueId,
883-
// parentAttributeValueId: props.attributeDef.parentValueId,
884-
// propId: props.attributeDef.propId,
885-
// componentId,
886-
// value: null,
887-
// },
888-
// });
889-
890880
attributesStore.RESET_PROPERTY_VALUE({
891881
attributeValueId: props.attributeDef.valueId,
892882
});
@@ -911,7 +901,7 @@ function updateValue() {
911901
if (newVal === "" && !currentValue.value) skipUpdate = true;
912902
}
913903
914-
// dont trigger an update if the value has not changed
904+
// don't trigger an update if the value has not changed
915905
// (and some special cases handled for specific types)
916906
if (skipUpdate || newVal === currentValue.value) {
917907
return;
@@ -970,7 +960,7 @@ const secret = computed(
970960
const secretDefinitionId = computed(() => {
971961
if (props.attributeDef.propDef.widgetKind.kind !== "secret") return;
972962
const widgetOptions = props.attributeDef.propDef.widgetKind.options;
973-
// WHAT? this setup doesn't really make sense...
963+
// A widget of kind=secret has a single option that points to its secret definition
974964
const secretKind = _.find(
975965
widgetOptions,
976966
(o) => o.label === "secretKind",

lib/dal/src/attribute/prototype.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use si_layer_cache::LayerDbError;
1818
use telemetry::prelude::*;
1919
use thiserror::Error;
2020

21+
use crate::attribute::prototype::argument::value_source::ValueSource;
22+
use crate::attribute::prototype::argument::AttributePrototypeArgument;
2123
use crate::change_set_pointer::ChangeSetPointerError;
2224
use crate::layer_db_types::{AttributePrototypeContent, AttributePrototypeContentV1};
2325
use crate::workspace_snapshot::content_address::{ContentAddress, ContentAddressDiscriminants};
@@ -29,14 +31,17 @@ use crate::workspace_snapshot::node_weight::{
2931
};
3032
use crate::workspace_snapshot::WorkspaceSnapshotError;
3133
use crate::{
32-
pk, AttributeValueId, DalContext, FuncId, OutputSocketId, PropId, Timestamp, TransactionsError,
34+
pk, AttributeValueId, DalContext, FuncId, InputSocketId, OutputSocketId, PropId, Timestamp,
35+
TransactionsError,
3336
};
3437

3538
pub mod argument;
3639

3740
#[remain::sorted]
3841
#[derive(Error, Debug)]
3942
pub enum AttributePrototypeError {
43+
#[error("attribute prototype argument error: {0}")]
44+
AttributePrototypeArgument(String),
4045
#[error("change set error: {0}")]
4146
ChangeSet(#[from] ChangeSetPointerError),
4247
#[error("edge weight error: {0}")]
@@ -378,4 +383,25 @@ impl AttributePrototype {
378383

379384
Ok(())
380385
}
386+
387+
pub async fn list_input_socket_sources_for_id(
388+
ctx: &DalContext,
389+
ap_id: AttributePrototypeId,
390+
) -> AttributePrototypeResult<Vec<InputSocketId>> {
391+
let apa_ids = AttributePrototypeArgument::list_ids_for_prototype(ctx, ap_id)
392+
.await
393+
.map_err(|e| AttributePrototypeError::AttributePrototypeArgument(e.to_string()))?;
394+
395+
let mut input_socket_ids = Vec::<InputSocketId>::new();
396+
for apa_id in apa_ids {
397+
let maybe_value_source = AttributePrototypeArgument::value_source_by_id(ctx, apa_id)
398+
.await
399+
.map_err(|e| AttributePrototypeError::AttributePrototypeArgument(e.to_string()))?;
400+
if let Some(ValueSource::InputSocket(socket_id)) = maybe_value_source {
401+
input_socket_ids.push(socket_id);
402+
}
403+
}
404+
405+
Ok(input_socket_ids)
406+
}
381407
}

lib/dal/src/attribute/value.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2106,23 +2106,11 @@ impl AttributeValue {
21062106
Ok(())
21072107
}
21082108

2109-
pub async fn list_input_sockets_sources_for_id(
2109+
pub async fn list_input_socket_sources_for_id(
21102110
ctx: &DalContext,
21112111
av_id: AttributeValueId,
21122112
) -> AttributeValueResult<Vec<InputSocketId>> {
21132113
let prototype_id = Self::prototype_id(ctx, av_id).await?;
2114-
2115-
let apa_ids = AttributePrototypeArgument::list_ids_for_prototype(ctx, prototype_id).await?;
2116-
2117-
let mut input_socket_ids = Vec::<InputSocketId>::new();
2118-
for apa_id in apa_ids {
2119-
let maybe_value_source =
2120-
AttributePrototypeArgument::value_source_by_id(ctx, apa_id).await?;
2121-
if let Some(ValueSource::InputSocket(socket_id)) = maybe_value_source {
2122-
input_socket_ids.push(socket_id);
2123-
}
2124-
}
2125-
2126-
Ok(input_socket_ids)
2114+
Ok(AttributePrototype::list_input_socket_sources_for_id(ctx, prototype_id).await?)
21272115
}
21282116
}

lib/dal/src/prop.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ use crate::workspace_snapshot::edge_weight::{EdgeWeight, EdgeWeightKind};
2424
use crate::workspace_snapshot::edge_weight::{EdgeWeightError, EdgeWeightKindDiscriminants};
2525
use crate::workspace_snapshot::node_weight::{NodeWeight, NodeWeightError};
2626
use crate::workspace_snapshot::WorkspaceSnapshotError;
27-
use crate::AttributeValueId;
2827
use crate::{
2928
label_list::ToLabelList, pk, property_editor::schema::WidgetKind, AttributePrototype,
3029
AttributePrototypeId, DalContext, Func, FuncBackendResponseType, FuncId, SchemaVariantId,
3130
Timestamp, TransactionsError,
3231
};
32+
use crate::{AttributeValueId, InputSocketId};
3333

3434
pub const PROP_VERSION: PropContentDiscriminants = PropContentDiscriminants::V1;
3535

@@ -762,6 +762,11 @@ impl Prop {
762762
.into())
763763
}
764764

765+
pub async fn input_socket_sources(&self, ctx: &DalContext) -> PropResult<Vec<InputSocketId>> {
766+
let prototype_id = Self::prototype_id(ctx, self.id).await?;
767+
Ok(AttributePrototype::list_input_socket_sources_for_id(ctx, prototype_id).await?)
768+
}
769+
765770
pub async fn set_default_value<T: Serialize>(
766771
ctx: &DalContext,
767772
prop_id: PropId,

lib/dal/src/property_editor/schema.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl PropertyEditorSchema {
3232
let root_prop_id =
3333
Prop::find_prop_id_by_path(ctx, schema_variant_id, &PropPath::new(["root"])).await?;
3434
let root_prop = Prop::get_by_id(ctx, root_prop_id).await?;
35-
let root_property_editor_prop = PropertyEditorProp::new(root_prop);
35+
let root_property_editor_prop = PropertyEditorProp::new(ctx, root_prop).await?;
3636
let root_property_editor_prop_id = root_property_editor_prop.id;
3737
props.insert(root_property_editor_prop_id, root_property_editor_prop);
3838

@@ -72,7 +72,7 @@ impl PropertyEditorSchema {
7272
// NOTE(nick): we already have the node weight, but I believe we still want to use "get_by_id" to
7373
// get the content from the store. Perhaps, there's a more efficient way that we can do this.
7474
let child_prop = Prop::get_by_id(ctx, child_prop_id).await?;
75-
let child_property_editor_prop = PropertyEditorProp::new(child_prop);
75+
let child_property_editor_prop = PropertyEditorProp::new(ctx, child_prop).await?;
7676

7777
// Load the work queue with the child prop.
7878
work_queue.push_back((child_prop_id, child_property_editor_prop.id));
@@ -104,11 +104,14 @@ pub struct PropertyEditorProp {
104104
pub doc_link: Option<String>,
105105
pub documentation: Option<String>,
106106
pub validation_format: Option<String>,
107+
pub default_can_be_set_by_socket: bool,
107108
}
108109

109110
impl PropertyEditorProp {
110-
pub fn new(prop: Prop) -> PropertyEditorProp {
111-
PropertyEditorProp {
111+
pub async fn new(ctx: &DalContext, prop: Prop) -> PropertyEditorResult<PropertyEditorProp> {
112+
let default_can_be_set_by_socket = !prop.input_socket_sources(ctx).await?.is_empty();
113+
114+
Ok(PropertyEditorProp {
112115
id: prop.id.into(),
113116
name: prop.name,
114117
kind: prop.kind.into(),
@@ -119,7 +122,8 @@ impl PropertyEditorProp {
119122
doc_link: prop.doc_link.map(Into::into),
120123
documentation: prop.documentation.map(Into::into),
121124
validation_format: prop.validation_format.map(Into::into),
122-
}
125+
default_can_be_set_by_socket,
126+
})
123127
}
124128
}
125129

lib/dal/src/property_editor/values.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ impl PropertyEditorValues {
135135
let child_property_editor_value_id = PropertyEditorValueId::from(child_av_id);
136136

137137
let sockets_for_av =
138-
AttributeValue::list_input_sockets_sources_for_id(ctx, child_av_id).await?;
138+
AttributeValue::list_input_socket_sources_for_id(ctx, child_av_id).await?;
139139

140140
let is_from_external_source = sockets_for_av
141141
.iter()

lib/dal/src/schema/variant.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ impl SchemaVariant {
621621
/// [`AttributePrototypeArgument`](crate::attribute::prototype::argument::AttributePrototypeArgument)
622622
/// for the /root/si/color [`Prop`](crate::Prop). If a prototype already
623623
/// exists pointing to a function other than
624-
/// [`IntrinsicFunc::SetString`](`crate::func::intrinsics::IntrinsicFunc::SetString`)
624+
/// [`IntrinsicFunc::SetString`](crate::func::intrinsics::IntrinsicFunc::SetString)
625625
/// we will remove that edge and replace it with one pointing to
626626
/// `SetString`.
627627
pub async fn set_color(
@@ -642,7 +642,7 @@ impl SchemaVariant {
642642
/// [`AttributePrototypeArgument`](crate::attribute::prototype::argument::AttributePrototypeArgument)
643643
/// for the /root/si/type [`Prop`](crate::Prop). If a prototype already
644644
/// exists pointing to a function other than
645-
/// [`IntrinsicFunc::SetString`](`crate::func::intrinsics::IntrinsicFunc::SetString`)
645+
/// [`IntrinsicFunc::SetString`](crate::func::intrinsics::IntrinsicFunc::SetString)
646646
/// we will remove that edge and replace it with one pointing to
647647
/// `SetString`.
648648
pub async fn set_type(
@@ -663,7 +663,7 @@ impl SchemaVariant {
663663
/// [`AttributePrototypeArgument`](crate::attribute::prototype::argument::AttributePrototypeArgument)
664664
/// for the /root/si/type [`Prop`](crate::Prop). If a prototype already
665665
/// exists pointing to a function other than
666-
/// [`IntrinsicFunc::SetString`](`crate::func::intrinsics::IntrinsicFunc::SetString`)
666+
/// [`IntrinsicFunc::SetString`](crate::func::intrinsics::IntrinsicFunc::SetString)
667667
/// we will remove that edge and replace it with one pointing to
668668
/// `SetString`.
669669
pub async fn get_type(&self, ctx: &DalContext) -> SchemaVariantResult<Option<ComponentType>> {

lib/dal/src/workspace_snapshot/edge_weight.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub enum EdgeWeightKind {
4242
Ordinal,
4343
/// Used to link an attribute value to the prop that it is for.
4444
Prop,
45-
/// An edge from a [`socket`](crate::socket) or an [`AttributeValue`](`crate::AttributeValue`)
45+
/// An edge from a [`socket`](crate::socket), [`AttributeValue`](crate::AttributeValue) or [`Prop`](crate::Prop)
4646
/// to an [`AttributePrototype`](crate::AttributePrototype). The optional [`String`] is used for
4747
/// maps, arrays and relevant container types to indicate which element the prototype is for.
4848
Prototype(Option<String>),

0 commit comments

Comments
 (0)