Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9c04a99
MWPW-188624: support promo override in grouped variations
yesil Apr 2, 2026
1523ab4
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 2, 2026
de3878b
support editor
yesil Apr 2, 2026
6148c9b
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 13, 2026
b1dc910
Merge branch 'main' into MWPW-188624
mirafedas Apr 13, 2026
fe7c3a5
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 13, 2026
1711b93
Merge branch 'MWPW-188624' of github.com:adobecom/mas into MWPW-188624
yesil Apr 13, 2026
270f520
fix test
yesil Apr 14, 2026
19f491a
fix test
yesil Apr 14, 2026
3be07f2
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 15, 2026
00b52bd
pr review
yesil Apr 15, 2026
d361444
Merge branch 'main' into MWPW-188624
yesil Apr 16, 2026
9b6d0ce
Merge branch 'main' into MWPW-188624
afmicka Apr 16, 2026
0511747
Merge branch 'main' into MWPW-188624
afmicka Apr 16, 2026
678d470
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 23, 2026
1717b19
introduce compatVersion
yesil Apr 23, 2026
1258bbd
introduce compatVersion
yesil Apr 23, 2026
f440fd0
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 27, 2026
aa31430
pass promo code to checkout links as well
yesil Apr 27, 2026
0c45866
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 28, 2026
563b7d7
cover other fragment creation use cases
yesil Apr 28, 2026
f94b93c
cover other fragment creation use cases
yesil Apr 28, 2026
9f887c6
fix bundle
yesil Apr 28, 2026
ec9ede4
enable save on apply
yesil Apr 28, 2026
ef8b5c3
set compatVersion only if actual value is smaller
yesil Apr 28, 2026
a2647dc
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 29, 2026
cc6ccb2
set compatVersion only if promoCode is set
yesil Apr 29, 2026
4c55839
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 29, 2026
28b18a9
Merge branch 'main' of github.com:adobecom/mas into MWPW-188624
yesil Apr 29, 2026
ee3691d
handle edge case after fragment refresh
yesil Apr 29, 2026
2d21e87
Merge branch 'main' into MWPW-188624
yesil Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions studio/src/editors/merch-card-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,54 @@ import { getItemFieldStateByIndex } from '../utils/field-state.js';
import { Fragment } from '../aem/fragment.js';
import { toAttribute } from '../aem/tag-path-utils.js';
import { getGlobalSettingsDefaults } from '../settings/settings-store.js';
import { getLocaleByCode } from '../../../io/www/src/fragment/locales.js';

const QUANTITY_MODEL = 'quantitySelect';
const WHAT_IS_INCLUDED = 'whatsIncluded';
const EVENT_COMMERCE_READY = 'wcms:commerce:ready';
const INLINE_PRICE_SELECTOR = 'span[is="inline-price"][data-wcs-osi]';

function isEditorPriceElement(element) {
if (element.closest('#preview-wrapper')) return true;
const host = element.getRootNode()?.host;
return host?.nodeName === 'RTE-FIELD' && !!host.closest('merch-card-editor');
}

export function getActiveMerchCardEditor() {
return document.querySelector('merch-card-editor');
}

function groupedPreviewLocaleProvider(element, options) {
if (!isEditorPriceElement(element)) return;
const localeCode = getActiveMerchCardEditor()?.previewLocaleOverride;
if (!localeCode) return;

const locale = getLocaleByCode(localeCode);
if (!locale) return;

options.locale = localeCode;
options.language = locale.lang;
options.country = locale.country;
}

function registerGroupedPreviewLocaleProvider(service) {
if (!service?.providers?.price) return;
if (service.providers.has(groupedPreviewLocaleProvider)) return;
service.providers.price(groupedPreviewLocaleProvider);
}

function editorPromoCodeProvider(element, options) {
if (!isEditorPriceElement(element)) return;
const promoCode = getActiveMerchCardEditor()?.getEffectiveFieldValue('promoCode', 0);
if (!promoCode) return;
options.promotionCode = promoCode;
}

function registerEditorPromoCodeProvider(service) {
if (!service?.providers?.price) return;
if (service.providers.has(editorPromoCodeProvider)) return;
service.providers.price(editorPromoCodeProvider);
}

const VARIANT_RTE_MARKS = {
[VARIANT_NAMES.MINI]: {
Expand All @@ -41,6 +86,7 @@ class MerchCardEditor extends LitElement {
localeDefaultFragment: { type: Object, attribute: false },
isVariation: { type: Boolean, attribute: false },
fieldsReady: { type: Boolean, state: true },
previewLocaleOverride: { type: String, state: true },
};

static SECTION_FIELDS = {
Expand Down Expand Up @@ -72,6 +118,7 @@ class MerchCardEditor extends LitElement {
this.isVariation = false;
this.lastMnemonicState = null;
this.fieldsReady = false;
this.previewLocaleOverride = null;
this.localeSearch = '';
this.reactiveController = new ReactiveController(this, []);
}
Expand All @@ -92,6 +139,44 @@ class MerchCardEditor extends LitElement {
return (this.fragment.getFieldValues('pznTags') || []).filter(Boolean).join(',');
}

#normalizeGroupedPreviewLocaleCode(tagValue) {
const localeCode = tagValue?.split('/').pop()?.trim();
return getLocaleByCode(localeCode) ? localeCode : null;
}

get groupedPreviewLocales() {
if (!this.isGroupedVariation) return [];
const tags = this.fragment?.getFieldValues('pznTags') || [];
const localeCodes = [...new Set(tags.map((tag) => this.#normalizeGroupedPreviewLocaleCode(tag)).filter(Boolean))];
return localeCodes.map((code) => {
const locale = getLocaleByCode(code);
return {
code,
lang: locale.lang,
country: locale.country,
label: `${locale.country} (${locale.lang.toUpperCase()})`,
};
});
}

#syncGroupedPreviewLocale() {
const locales = this.groupedPreviewLocales;
if (!locales.length) {
if (this.previewLocaleOverride !== null) {
this.previewLocaleOverride = null;
}
return;
}

const codes = locales.map((locale) => locale.code);
const globalLocale = Store.localeOrRegion();
this.previewLocaleOverride = codes.includes(this.previewLocaleOverride)
? this.previewLocaleOverride
: codes.includes(globalLocale)
? globalLocale
: codes[0];
}

#normalizePznTagIds(value) {
const rawValues = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
return [
Expand Down Expand Up @@ -434,17 +519,41 @@ class MerchCardEditor extends LitElement {

connectedCallback() {
super.connectedCallback();
this.#registerCommercePriceProviders();
document.addEventListener(EVENT_COMMERCE_READY, this.#handleCommerceReady);
}

disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener(EVENT_COMMERCE_READY, this.#handleCommerceReady);
this.lastMnemonicState = null;
}

#handleCommerceReady = (event) => {
this.#registerCommercePriceProviders(event.detail);
};

#registerCommercePriceProviders(service = document.querySelector('mas-commerce-service')) {
registerGroupedPreviewLocaleProvider(service);
registerEditorPromoCodeProvider(service);
}

refreshRenderedPrices() {
document.querySelector('mas-commerce-service')?.refreshOffers?.();
this.querySelectorAll('rte-field').forEach((field) => {
field.shadowRoot?.querySelectorAll(INLINE_PRICE_SELECTOR).forEach((price) => {
price.requestUpdate(true);
Comment thread
honstar marked this conversation as resolved.
});
});
}

#cachedGlobalDefaults = null;

willUpdate(changedProperties) {
this.#cachedGlobalDefaults = null;
if (this.fragmentStore?.get()) {
this.#syncGroupedPreviewLocale();
Comment thread
honstar marked this conversation as resolved.
}
if (changedProperties.has('fragmentStore') && this.fragmentStore) {
this.fieldsReady = false;
this.reactiveController.updateStores([this.fragmentStore, Store.settings.rows, Store.search]);
Expand Down Expand Up @@ -611,6 +720,16 @@ class MerchCardEditor extends LitElement {

async updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('previewLocaleOverride')) {
this.refreshRenderedPrices();
this.dispatchEvent(
new CustomEvent('preview-locale-change', {
bubbles: true,
composed: true,
detail: { value: this.previewLocaleOverride },
}),
);
}
if (!this.fieldsReady && this.fragment) {
await this.updateComplete;
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
Expand Down
60 changes: 60 additions & 0 deletions studio/src/mas-fragment-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from './constants.js';
import router from './router.js';
import { VARIANTS } from './editors/variant-picker.js';
import { getActiveMerchCardEditor } from './editors/merch-card-editor.js';
import { extractLocaleFromPath, generateCodeToUse, getFragmentMapping, replaceLocaleInPath, showToast } from './utils.js';
import { getSpectrumVersion } from './constants/icon-library.js';
import './editors/merch-card-editor.js';
Expand Down Expand Up @@ -98,6 +99,24 @@ export default class MasFragmentEditor extends LitElement {
overflow-y: auto;
}

.preview-locale-panel {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
padding: 20px 20px 0 20px;
box-sizing: border-box;
}

.preview-locale-panel sp-field-label {
font-size: 14px;
color: var(--spectrum-global-color-gray-800);
}

.preview-locale-panel sp-picker {
width: 100%;
}

@media (max-width: 1200px) {
#preview-column {
position: relative;
Expand Down Expand Up @@ -1065,6 +1084,9 @@ export default class MasFragmentEditor extends LitElement {
}

this.fragmentStore.updateField(fieldName, value);
if (fieldName === 'promoCode') {
getActiveMerchCardEditor()?.refreshRenderedPrices?.();
}
}

async deleteFragment() {
Expand Down Expand Up @@ -1355,6 +1377,42 @@ export default class MasFragmentEditor extends LitElement {
</div>`;
}

#handleGroupedPreviewLocaleChange = (event) => {
const editor = getActiveMerchCardEditor();
if (!editor) return;
editor.previewLocaleOverride = event.target.value || null;
};

#handlePreviewLocaleChange = (event) => {
if (!this.fragmentStore?.previewStore) return;
const localeValue = event.detail?.value ?? null;
const changed = this.fragmentStore.previewStore.setPreviewLocaleOverride(localeValue);
if (!changed) return;
this.fragmentStore.previewStore.resolveFragment();
this.requestUpdate();
};

get groupedPreviewLocaleSelector() {
const editor = getActiveMerchCardEditor();
const locales = editor?.groupedPreviewLocales || [];
if (!locales.length) return nothing;

const selectedLocale = editor?.previewLocaleOverride || locales[0].code;

return html`
<div class="preview-locale-panel">
<sp-field-label for="grouped-preview-locale">Preview card for:</sp-field-label>
<sp-picker
id="grouped-preview-locale"
value="${selectedLocale}"
@change=${this.#handleGroupedPreviewLocaleChange}
>
${locales.map((locale) => html`<sp-menu-item value="${locale.code}">${locale.label}</sp-menu-item>`)}
</sp-picker>
</div>
`;
}

get localeVariationHeader() {
if (!this.fragment || !this.editorContextStore.isVariation(this.fragment.id)) {
return nothing;
Expand Down Expand Up @@ -1527,6 +1585,7 @@ export default class MasFragmentEditor extends LitElement {
.updateFragment=${this.updateFragment}
.localeDefaultFragment=${this.localeDefaultFragment}
.isVariation=${this.editorContextStore.isVariation(this.fragment?.id)}
@preview-locale-change=${this.#handlePreviewLocaleChange}
></merch-card-editor>
`;
break;
Expand Down Expand Up @@ -1565,6 +1624,7 @@ export default class MasFragmentEditor extends LitElement {
return html`
<div id="preview-column">
<div id="preview-wrapper">
${this.groupedPreviewLocaleSelector}
${this.editorContextStore.isVariation(this.fragment.id)
? Fragment.isGroupedVariationPath(this.fragment.path)
? this.displayGroupedVariationInfo('preview-header')
Expand Down
13 changes: 12 additions & 1 deletion studio/src/reactivity/preview-fragment-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function mergeResolvedPreviewFields(originalFields = [], resolvedFields =
export class PreviewFragmentStore extends FragmentStore {
resolved = false;
placeholderUnsubscribe = null;
previewLocaleOverride = null;
#resolving = false;
#resolveDebounceTimer = null;
#refreshDebounceTimer = null;
Expand Down Expand Up @@ -135,6 +136,16 @@ export class PreviewFragmentStore extends FragmentStore {
this.resolveFragment();
}

setPreviewLocaleOverride(value) {
const nextValue = value || null;
if (this.previewLocaleOverride === nextValue) {
return false;
}
this.previewLocaleOverride = nextValue;
this.resolved = false;
return true;
}

resolveFragment(immediate = false) {
this.lazy = false;
clearTimeout(this.#resolveDebounceTimer);
Expand Down Expand Up @@ -213,7 +224,7 @@ export class PreviewFragmentStore extends FragmentStore {
body.fields = serializePreviewFields(originalFields);

const context = {
locale: Store.localeOrRegion(),
locale: this.previewLocaleOverride || Store.localeOrRegion(),
surface: Store.surface(),
dictionary: Store.placeholders.preview.value,
};
Expand Down
5 changes: 4 additions & 1 deletion studio/src/reactivity/source-fragment-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ export class SourceFragmentStore extends FragmentStore {
return success;
}

resolvePreviewFragment() {
resolvePreviewFragment(previewLocaleOverride) {
if (previewLocaleOverride !== undefined) {
this.previewStore.setPreviewLocaleOverride(previewLocaleOverride);
}
this.previewStore.resolveFragment();
}

Expand Down
5 changes: 5 additions & 0 deletions studio/src/rte/ost.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export function openOfferSelectorTool(triggerElement, offerElement) {
let initialReferenceOsi;
const aosAccessToken = localStorage.getItem('masAccessToken') ?? window.adobeid.authorize();
const searchParameters = new URLSearchParams();
const promotionCode = triggerElement?.closest('merch-card-editor')?.getEffectiveFieldValue('promoCode', 0)?.trim();

const offerSelectorPlaceholderOptions = {};
if (offerElement) {
Expand All @@ -223,6 +224,10 @@ export function openOfferSelectorTool(triggerElement, offerElement) {
}
});

if (promotionCode && !offerSelectorPlaceholderOptions.promotionCode) {
offerSelectorPlaceholderOptions.promotionCode = promotionCode;
}

[
'promotionCode', // contextual promo code (e.g. set on card/)
'storedPromoOverride', // promo code set directly on price/CTA
Expand Down
4 changes: 3 additions & 1 deletion studio/src/rte/rte-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,7 @@ class RteField extends LitElement {
const parser = DOMParser.fromSchema(this.#editorSchema);
const doc = parser.parse(container);
const tr = this.editorView.state.tr.replaceWith(0, this.editorView.state.doc.content.size, doc.content);
tr.setMeta('external', true);
this.editorView.dispatch(tr);
} catch (error) {
console.error('Error updating editor content:', error);
Expand All @@ -1217,11 +1218,12 @@ class RteField extends LitElement {
const value = this.#serializeContent(newState);
// skip change event during initialization
const isFirstChange = transaction.getMeta('initialize');
const isExternalUpdate = transaction.getMeta('external');
if (value !== this.value) {
this.#isInternalUpdate = true;
this.value = value === '<p></p>' ? '' : value;
this.#isInternalUpdate = false;
if (isFirstChange) return;
if (isFirstChange || isExternalUpdate) return;
this.dispatchEvent(
new CustomEvent('change', {
bubbles: true,
Expand Down
16 changes: 16 additions & 0 deletions studio/test/rte/rte-field.test.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@
const rte = await createFromTemplate('rte', this.test.title);
expect(rte.shadowRoot).exist;
});

it('should not emit change when syncing an external value update', async function () {
const rte = await createFromTemplate('rte-link', this.test.title);
let changeCount = 0;
rte.addEventListener('change', () => {
changeCount += 1;
});

rte.value = '<a class="accent" data-checkout-workflow="UCv3" data-wcs-osi="abm">Buy now</a>';
await delay(100);

expect(changeCount).to.equal(0);
expect(rte.value).to.equal(
'<p><a class="accent" data-checkout-workflow="UCv3" data-wcs-osi="abm">Buy now</a></p>',
);
});
});

describe('RTE Field: styling features', () => {
Expand Down
2 changes: 1 addition & 1 deletion web-components/dist/mas.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion web-components/dist/merch-card.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions web-components/src/merch-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ function priceOptionsProvider(element, options) {
options.literals ??= {};
Object.assign(options.literals, card.priceLiterals);
}
if (!options.promotionCode) {
options.promotionCode = card.promotionCode;
}
if (card.aemFragment) {
options[FF_DEFAULTS] = true;
}
Expand Down
Loading
Loading