Version component prop definitions for SDC and Code components

Created on 12 May 2025, 1 day ago

Overview

At present `sourceTypeSettings` and `sourceType` in stored in the data model in the inputs column. This is because things like `hook_storage_prop_shape_alter` can modify the data shape used for a component based on the presence/absence of typed-data types. These can be quite large, see this example from in XB's tests

Additionally a component (SDC or Code) definition can change which can have an impact on instance settings. For example if a string prop in an SDC component is changed to an integer or a new value is added to an enum this can change either the source type or settings..

Proposed resolution

Change the definition of the `prop_field_definitions` settings for both SDC and Code components. Instead of making it a point-in-time definition of the props definitions - make it a version history.

These are the scenarios that would constitute a new version:

  1. Adding a new required prop - this is relevant if we go down the non JSON storage approach
  2. Removing a value from an enum
  3. Changing the mapped field type
  4. Removing a prop - although the net outcome here is redundant data in the database for old components - so we might not care here

If we take those scenarios as inputs, we should be able to create a determinstic hashing approach that gives us a unique version ID for each component and set of those constraints.

We then should be able to store this hash as a version ID along with the component ID in the component tree.

Before

[
  ComponentTreeStructure::ROOT_UUID => [
    [
      'uuid' => 'two-column-uuid',
      'component' => 'sdc.experience_builder.two_column',
    ],
  ],
  'two-column-uuid' => [
    'column_one' => [
      [
        'uuid' => 'static-image-udf7d',
        'component' => 'sdc.experience_builder.image',
      ],
    ],
    'column_two' => [],
  ],
];
prop_field_definitions:
  heading:
    field_type: string
    field_storage_settings: {  }
    field_instance_settings: {  }
    field_widget: string_textfield
    default_value:
      -
        value: 'There goes my hero'
    expression: ℹ︎string␟value
  cta1href:
    field_type: link
    field_storage_settings: {  }
    field_instance_settings:
      title: 0
    field_widget: link_default
    default_value:
      -
        uri: 'https://example.com'
        options: {  }
    expression: ℹ︎link␟uri

After

[
  ComponentTreeStructure::ROOT_UUID => [
    [
      'uuid' => 'two-column-uuid',
      'component' => 'sdc.experience_builder.two_column',
      'version' => 'sIXdVsZK0yE', // 👈️ Prop definition version ID
    ],
  ],
  'two-column-uuid' => [
    'column_one' => [
      [
        'uuid' => 'static-image-udf7d',
        'component' => 'sdc.experience_builder.image',
        'version' => 'JwX5oCE8Osp', // 👈️ Prop definition version ID 
      ],
    ],
    'column_two' => [],
  ],
];
prop_field_definitions:
  current: sIXdVsZK0yE # 👈️ Store the hash of the current version
  sIXdVsZK0yE: # 👈️ Keep each historic version, keyed by a hash
    heading:
      field_type: string
      field_storage_settings: {  }
      field_instance_settings: {  }
      field_widget: string_textfield
      default_value:
        -
          value: 'There goes my hero'
      expression: ℹ︎string␟value
    cta1href:
      field_type: link
      field_storage_settings: {  }
      field_instance_settings:
        title: 0
      field_widget: link_default
      default_value:
        -
          uri: 'https://example.com'
          options: {  }
      expression: ℹ︎link␟uri
  vfTzLuF89Xp: # 👈️ This is an older version
    heading:
      field_type: string
      field_storage_settings: {  }
      field_instance_settings: {  }
      field_widget: string_textfield
      default_value:
        -
          value: 'There goes my hero'
      expression: ℹ︎string␟value
    cta1href:
      field_type: uri # 👈️ Different field type
      field_storage_settings: {  }
      field_instance_settings: {  } # 👈️ Different field settings
      field_widget: uri # 👈️ Different widget
      default_value:
        -
          value: 'https://example.com'
      expression: ℹ︎uri␟value # 👈️ Different data type

User interface changes

📌 Task
Status

Active

Version

0.0

Component

Component sources

Created by

🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @larowlan
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    .

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    This is very similar to the outline I wrote on 🌱 [META] Production-ready ComponentSource plugins Active .

    The key difference: I tried to do it generically, not restricted to SDCs & code components. So, for example, Block plugins may change (evolve) their settings and provide an update path, and so XB should be able to apply such an update path. Layout Builder hasn't solved this yet either — see the 🐛 Block plugins need to be able to update their settings in multiple different storage implementations Active core issue

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Specifically doing this for only the sdc and js component sources does not make that much sense to me yet. What problem does that solve? 🤔

    To avoid the need to look up in some central place what field type + storage settings + instance settings + widget should be used for a given SDC prop is … exactly why StaticPropSources contain all that information, and why that is stored in the inputs field property of the XB field type.

    And … I think that is what you're aiming to solve here: storing less data in inputs. Even though you omitted it from the issue summary: you only showed the before vs after for the tree field property, not for the inputs field property 😄 Updated issue summary based on this interpretation. But I'd like you to confirm it. 🙏

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Very closely related, and one of the oldest XB issues: 📌 [later phase] When the field type for a PropShape changes, the Content Creator must be able to upgrade Postponed .

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    From @larowlan's issue summary:

    1. Adding a new required prop - this is relevant if we go down the non JSON storage approach
    2. Removing a value from an enum
    3. Changing the mapped field type
    4. Removing a prop - although the net outcome here is redundant data in the database for old components - so we might not care here

    I don't think I agree with stating these are things we have to support:

    1. This is sort of a BC break, except we could automatically fix this: detect the stored version not matching the current version, and add the missing default — both at render time and at edit time, which means after saving it'll have been automatically fixed.
    2. This is a BC break we cannot recover from: either we drop the user's choice (data loss) or the rendering crashes — which is exactly why 📌 Improve server-side error handling Active has explicit test coverage for this.
    3. This is what hook_storage_prop_shape_alter() allows and what #5 is about. And it's what allows the storage efficiency improvement described in #4.
    4. Equivalent to point 1, except that here too there would be data loss — especially if a future revision of the SDC restores this. Note that redundant data (aka prop sources for non-existent props) will not be accepted by \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\GeneratedFieldExplicitInputUxComponentSourceBase::validateComponentInput().

    So: what is your purpose with this issue, @larowlan? Storage efficiency, input schema evolvability, both, or something else still?

Production build 0.71.5 2024