Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion studio/src/aem/fragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export class Fragment {
const prepared = new Fragment(this);

// Fields that should never be reset (they're fragment-specific, not inherited)
const excludeFields = ['variations', 'tags', 'originalId', 'locReady'];
const excludeFields = ['variations', 'tags', 'originalId', 'locReady', 'compatVersion'];

for (const field of prepared.fields) {
if (excludeFields.includes(field.name)) continue;
Expand Down
10 changes: 10 additions & 0 deletions studio/src/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { COMPAT_VERSION_GLOBAL_PROMO_CODE } from '../../web-components/src/compat-version.js';

export const CHECKOUT_CTA_TEXTS = {
'buy-now': 'Buy now',
'free-trial': 'Free trial',
Expand Down Expand Up @@ -264,5 +266,13 @@ export const TRANSLATIONS_ALLOWED_SURFACES = ['acom', 'acom-cc', 'acom-dc', 'exp

/** Plain preview origin — use for direct `.json` lookups (e.g. fil_PH placeholder fallback). */
export const ODIN_PREVIEW_ORIGIN = 'https://odinpreview.corp.adobe.com';

/**
* Compat version of the card.
* 0: assumed version for fragments before compat version was introduced.
* see web-components/src/compat-version.js for more details.
*/
export const COMPAT_VERSION = COMPAT_VERSION_GLOBAL_PROMO_CODE;

/** Freyja fragments API root on the preview origin — use as `preview.url` in pipeline contexts. */
export const ODIN_PREVIEW_FRAGMENTS_URL = `${ODIN_PREVIEW_ORIGIN}/adobe/contentFragments`;
141 changes: 140 additions & 1 deletion studio/src/editors/merch-card-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import '../aem/aem-tag-picker-field.js';
import './variant-picker.js';
import { SPECTRUM_COLORS } from '../utils/spectrum-colors.js';
import '../rte/osi-field.js';
import { CARD_MODEL_PATH } from '../constants.js';
import { CARD_MODEL_PATH, COMPAT_VERSION } from '../constants.js';
import '../fields/secure-text-field.js';
import '../fields/plan-type-field.js';
import { getFragmentMapping, showToast } from '../utils.js';
Expand All @@ -21,9 +21,49 @@ 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 editorPromoCodeProvider(element, options) {
if (!isEditorPriceElement(element)) return;
const promoCode = getActiveMerchCardEditor()?.getEffectiveFieldValue('promoCode', 0);
if (!promoCode) return;
options.promotionCode = promoCode;
}

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

const VARIANT_RTE_MARKS = {
[VARIANT_NAMES.MINI]: {
Expand All @@ -41,6 +81,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 +113,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 +134,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 +514,50 @@ class MerchCardEditor extends LitElement {

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

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

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

registerCommerceProviders(service = document.querySelector('mas-commerce-service')) {
if (!service?.providers) return;
if (!service.providers.has(groupedPreviewLocaleProvider)) {
service.providers.price(groupedPreviewLocaleProvider);
service.providers.checkout(groupedPreviewLocaleProvider);
}
if (!service.providers.has(editorPromoCodeProvider)) {
service.providers.price(editorPromoCodeProvider);
}
if (!service.providers.has(checkoutOptionsProvider)) {
service.providers.checkout(checkoutOptionsProvider);
}
}

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,17 +724,43 @@ 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 },
}),
);
}
this.ensurePromoCompatVersion();
if (!this.fieldsReady && this.fragment) {
await this.updateComplete;
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
this.toggleFields();
}
}

ensurePromoCompatVersion() {
if (!this.fragment) return;
const rawPromo = this.getEffectiveFieldValue('promoCode', 0);
const hasPromoCode = String(rawPromo ?? '').trim() !== '';
if (!hasPromoCode) return;

const rawCompat = this.getEffectiveFieldValue('compatVersion', 0);
const parsedCompat = Number(rawCompat);
const currentCompat = Number.isFinite(parsedCompat) ? parsedCompat : 0;
if (currentCompat < COMPAT_VERSION) {
this.fragmentStore.updateField('compatVersion', [String(COMPAT_VERSION)]);
}
}

async toggleFields() {
if (!this.fragment) {
return;
}

// Variations can inherit `variant` from their parent fragment.
// Use the effective value so template field visibility remains accurate.
const variantValue = this.getEffectiveFieldValue('variant');
Expand Down
3 changes: 2 additions & 1 deletion studio/src/mas-create-dialog.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LitElement, html, css, nothing } from 'lit';
import { EVENT_KEYDOWN, EVENT_OST_OFFER_SELECT, TAG_MODEL_ID_MAPPING } from './constants.js';
import { COMPAT_VERSION, EVENT_KEYDOWN, EVENT_OST_OFFER_SELECT, TAG_MODEL_ID_MAPPING } from './constants.js';
import router from './router.js';
import Store from './store.js';
import './rte/osi-field.js';
Expand Down Expand Up @@ -224,6 +224,7 @@ export class MasCreateDialog extends LitElement {
fragmentData.data = {
osi: this.osi,
tags: this.tags,
compatVersion: COMPAT_VERSION,
};
}

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 @@ -1082,6 +1101,9 @@ export default class MasFragmentEditor extends LitElement {
}

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

async deleteFragment() {
Expand Down Expand Up @@ -1372,6 +1394,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 @@ -1544,6 +1602,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 @@ -1582,6 +1641,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
Loading
Loading