Skip to main content

ClusterConfigurator

A variant selector for product clusters. Renders attribute-based configuration controls (dropdowns, radio pills, color swatches, image thumbnails) and resolves the matching product as the user makes selections. Supports drilldown filtering where each selection narrows the options available in subsequent attributes.

Usage

Basic usage on a cluster detail page

import ClusterConfigurator from '@/components/propeller/ClusterConfigurator';

function ClusterDetailPage({ cluster }) {
const [selectedProduct, setSelectedProduct] = useState(null);

return (
<ClusterConfigurator
clusterId={cluster.clusterId}
products={cluster.products}
config={cluster.config}
onConfigurationChange={(product) => setSelectedProduct(product)}
/>
);
}

Pre-selecting a default product

When landing on a cluster page via a direct product link, pass the product as defaultProduct to pre-populate all selectors on mount:

<ClusterConfigurator
clusterId={cluster.clusterId}
products={cluster.products}
config={cluster.config}
defaultProduct={activeProduct}
onConfigurationChange={(product) => setSelectedProduct(product)}
/>

With custom labels

<ClusterConfigurator
clusterId={cluster.clusterId}
products={cluster.products}
config={cluster.config}
labels={{ selectOption: '-- Choose an option --' }}
onConfigurationChange={(product) => setSelectedProduct(product)}
/>

Integrating with AddToCart

Pair the configurator with AddToCart so the resolved product can be added to the cart:

function ClusterPage({ cluster }) {
const [product, setProduct] = useState(cluster.products[0]);

return (
<div>
<ClusterConfigurator
clusterId={cluster.clusterId}
products={cluster.products}
config={cluster.config}
defaultProduct={product}
onConfigurationChange={setProduct}
/>
{product && (
<AddToCart
product={product}
graphqlClient={graphqlClient}
cartId={cart?.cartId}
/>
)}
</div>
);
}

With custom styling

<ClusterConfigurator
clusterId={cluster.clusterId}
products={cluster.products}
config={cluster.config}
className="max-w-md mx-auto"
onConfigurationChange={(product) => setSelectedProduct(product)}
/>

Configuration

Required

PropTypeDescription
clusterIdnumberThe cluster ID this configurator belongs to. Used to namespace radio button groups.
productsProduct[]All products belonging to the cluster. The component scans their attributes to derive available values and to match the configured product.
configClusterConfigThe cluster configuration object (cluster.config). Provides the ordered list of attribute settings that define which selectors to render.

Callbacks

PropTypeDescription
onConfigurationChange(product: Product) => voidFires when the user's selections uniquely identify a cluster product. Also fires on mount if defaultProduct is provided and all settings can be resolved.

Optional

PropTypeDefaultDescription
defaultProductProductA product to pre-populate all attribute selections on mount. The component reads its attributes to set initial values.
labelsRecord<string, string>{}Override UI strings. Currently supported keys: selectOption (dropdown placeholder, defaults to "-- Select --").
classNamestring''Extra CSS class applied to the root <div>.

Labels

KeyDefaultDescription
selectOption"-- Select --"Dropdown placeholder text

Behavior

Display Types

Each ClusterConfigSetting has a displayType that determines how the selector is rendered:

Display TypeRenderingInteraction
DROPDOWNStandard <select> element with a placeholder optionSelect from dropdown list
RADIOHorizontal row of pill-shaped buttonsClick to select; selected pill gets accent border and background
COLORCircular color swatches using the attribute value as backgroundColorClick to select; selected swatch gets ring highlight and slight scale-up
IMAGE64x64px thumbnail grid using the attribute value as image srcClick to select; selected thumbnail gets border highlight and a checkmark overlay
Any other valueRectangular pill buttons (same as RADIO but without hidden radio inputs)Click to select

Drilldown Filtering

Settings are sorted by priority (ascending) and rendered in that order. The component implements cascading drilldown logic:

  1. First attribute — All unique values across all cluster products are shown.
  2. Subsequent attributes — Only values that exist on products matching all prior selections are shown. This narrows options progressively.
  3. Disabled state — An attribute selector is disabled when either no values are available or any preceding attribute has not been selected yet.

Selection Cascading

When the user changes a selection:

  1. The selected value is stored for that attribute.
  2. All subsequent attribute selections are cleared.
  3. The component automatically pre-selects the first available value for each subsequent attribute (cascading forward).
  4. If all settings end up with a value (which is typical after any selection due to auto-fill), the matching product is resolved and onConfigurationChange fires.

This means that in most cases, a single user interaction resolves to a complete configuration immediately.

Default Product Initialization

When defaultProduct is provided:

  1. On mount, the component reads each setting's attribute value from the default product.
  2. All matching selections are populated.
  3. If all settings can be resolved, onConfigurationChange fires immediately with the matched product.

Product Matching

The findMatchingProduct algorithm iterates over all cluster products and returns the first one whose attributes match every key/value pair in the current selections. Matching is done by comparing attribute values extracted from AttributeResult objects.

Attribute Value Extraction

The component handles multiple Propeller SDK attribute value formats:

  • Current SDK formatAttributeType.COLOR, TEXT, DECIMAL, INT, ENUM (accessed via value.type and value.value)
  • Legacy SDK formatcolorValue, textValues, textValue, numericValue, booleanValue (accessed directly on the value object)
  • Fallback — Plain string values or generic object with a values array

Attribute Name Resolution

Attribute names are matched flexibly. The component checks:

  1. The first localised description value (attributeDescription.descriptions[0].value)
  2. The internal SDK name (attributeDescription.name)
  3. All localised descriptions for any match

This ensures the component works regardless of whether the ClusterConfigSetting.name uses the internal name or a localised label.


GraphQL Query Examples

Fetching a cluster with configuration and product attributes

query GetCluster($clusterId: Int!, $language: String) {
cluster(id: $clusterId, language: $language) {
clusterId
name {
value
language
}
config {
settings {
id
name
priority
displayType
}
}
products {
items {
productId
sku
name {
value
language
}
attributes {
items {
attributeDescription {
name
descriptions {
value
language
}
}
value {
type
value
}
}
}
price {
net
gross
}
media {
images {
url
}
}
}
}
}
}

Fetching only the configuration settings (lightweight)

query GetClusterConfig($clusterId: Int!) {
cluster(id: $clusterId) {
config {
settings {
id
name
priority
displayType
}
}
}
}

SDK Services

This component does not call any SDK services directly. It is a pure presentation and logic component that receives all data through props.

The parent page is responsible for fetching the cluster and its products. Typically this involves:

  • ClusterService.getCluster() — Fetches the cluster object including config (the ClusterConfig with settings) and products (all variant products with their attributes).

Required data shape

The component expects products to carry their attributes in product.attributes.items as an array of AttributeResult objects. Each AttributeResult must include:

  • attributeDescription.name — the internal attribute name (must match ClusterConfigSetting.name)
  • attributeDescription.descriptions[].value — localised display names
  • value — the attribute value in one of the supported SDK formats (see Attribute Value Extraction below)

The ClusterConfig must include:

  • settings[] — array of ClusterConfigSetting objects, each with id, name, priority, and displayType