diff --git a/blocks/edit/da-prepare/actions/msm/README.md b/blocks/edit/da-prepare/actions/msm/README.md new file mode 100644 index 00000000..6a4d96d3 --- /dev/null +++ b/blocks/edit/da-prepare/actions/msm/README.md @@ -0,0 +1,29 @@ +## Multi-Site Manager (MSM) +MSM enables base/satellite site relationships where satellite sites inherit content from a base site and can optionally override individual pages. + +### Configuration +MSM is configured via an `msm` sheet in the org-level DA config (`/config#/{org}/`). Each row defines a base-satellite relationship: + +| base | satellite | title | +| :--- | :--- | :--- | +| `my-base` | | Base Site | +| `my-base` | `satellite-1` | Satellite Site 1 | +| `my-base` | `satellite-2` | Satellite Site 2 | + +- The `base` column identifies the base site repo name. +- Rows with an empty `satellite` column define the base site entry and its display title. +- Rows with a `satellite` value define satellite sites that inherit from that base. + +### Features + +**Base site view** — when editing a page on the base site, the MSM panel shows all satellites split into inherited and custom (override) lists. Available actions: +- **Preview / Publish** — push the base page to inherited satellite sites via AEM. +- **Cancel inheritance** — copy the base page to a satellite, creating a local override. +- **Sync to satellite** — push updates to custom satellites via merge or full override. +- **Resume inheritance** — delete the satellite override so it falls back to the base. Automatically previews/publishes the page from the base based on the satellite's prior AEM status. + +Custom satellites always show an "Open in editor" link so base authors can inspect overrides. + +**Satellite site view** — when editing a page on a satellite site, the MSM panel shows the base site and offers: +- **Sync from Base** — pull latest base content via merge or full override. +- **Resume inheritance** — delete the local override. Automatically previews/publishes from the base based on prior AEM status. \ No newline at end of file diff --git a/blocks/edit/da-prepare/actions/msm/helpers/config.js b/blocks/edit/da-prepare/actions/msm/helpers/config.js new file mode 100644 index 00000000..952bdbd8 --- /dev/null +++ b/blocks/edit/da-prepare/actions/msm/helpers/config.js @@ -0,0 +1,92 @@ +import { DA_ORIGIN } from '../../../../../shared/constants.js'; +import { daFetch, fetchDaConfigs } from '../../../../../shared/utils.js'; + +const configCache = {}; + +async function fetchOrgMsmRows(org) { + const [orgConfig] = await Promise.all(fetchDaConfigs({ org })); + return orgConfig?.msm?.data || []; +} + +function resolveConfig(rows, site) { + const hasBaseCol = rows[0].base !== undefined; + + if (hasBaseCol) { + const baseRows = rows.filter((row) => row.base === site); + const satelliteRows = baseRows.filter((row) => row.satellite); + if (satelliteRows.length) { + const baseEntry = baseRows.find((row) => !row.satellite); + const satellites = satelliteRows.reduce((acc, row) => { + acc[row.satellite] = { label: row.title }; + return acc; + }, {}); + return { role: 'base', baseLabel: baseEntry?.title, satellites }; + } + const satRow = rows.find((row) => row.satellite === site); + if (satRow) { + const baseEntry = rows.find((row) => row.base === satRow.base && !row.satellite); + return { role: 'satellite', base: satRow.base, baseLabel: baseEntry?.title }; + } + return null; + } + + const isSatellite = rows.some((row) => row.satellite === site); + if (isSatellite) return null; + + const satellites = rows.reduce((acc, row) => { + if (row.satellite) acc[row.satellite] = { label: row.title }; + return acc; + }, {}); + return Object.keys(satellites).length ? { role: 'base', satellites } : null; +} + +async function fetchSiteConfig(org, site) { + const key = `${org}/${site}`; + if (configCache[key]) return configCache[key]; + + const rows = await fetchOrgMsmRows(org); + if (!rows.length) return null; + + const config = resolveConfig(rows, site); + if (!config) return null; + + configCache[key] = config; + return config; +} + +export async function getSatellites(org, baseSite) { + const config = await fetchSiteConfig(org, baseSite); + if (!config) return {}; + if (config.role === 'base') return config.satellites; + return {}; +} + +export async function getBaseSite(org, satellite) { + const config = await fetchSiteConfig(org, satellite); + if (!config) return null; + if (config.role === 'satellite') return config.base; + return null; +} + +export async function isPageLocal(org, site, pagePath) { + const resp = await daFetch( + `${DA_ORIGIN}/source/${org}/${site}${pagePath}.html`, + { method: 'HEAD' }, + ); + return resp.ok; +} + +export async function checkOverrides(org, satellites, pagePath) { + const entries = Object.entries(satellites); + const results = await Promise.all( + entries.map(async ([site, info]) => { + const local = await isPageLocal(org, site, pagePath); + return { site, label: info.label, hasOverride: local }; + }), + ); + return results; +} + +export function clearMsmCache() { + Object.keys(configCache).forEach((key) => { delete configCache[key]; }); +} diff --git a/blocks/edit/da-prepare/actions/msm/helpers/utils.js b/blocks/edit/da-prepare/actions/msm/helpers/utils.js new file mode 100644 index 00000000..a0e6ecef --- /dev/null +++ b/blocks/edit/da-prepare/actions/msm/helpers/utils.js @@ -0,0 +1,85 @@ +import { DA_ORIGIN } from '../../../../../shared/constants.js'; +import { daFetch } from '../../../../../shared/utils.js'; +import { getNx } from '../../../../../../scripts/utils.js'; + +const AEM_ADMIN = 'https://admin.hlx.page'; + +export async function previewSatellite(org, satellite, pagePath) { + const aemPath = pagePath.replace('.html', ''); + const url = `${AEM_ADMIN}/preview/${org}/${satellite}/main${aemPath}`; + const resp = await daFetch(url, { method: 'POST' }); + if (!resp.ok) { + const xError = resp.headers?.get('x-error') || `Preview failed (${resp.status})`; + return { error: xError }; + } + return resp.json(); +} + +export async function publishSatellite(org, satellite, pagePath) { + const aemPath = pagePath.replace('.html', ''); + const url = `${AEM_ADMIN}/live/${org}/${satellite}/main${aemPath}`; + const resp = await daFetch(url, { method: 'POST' }); + if (!resp.ok) { + const xError = resp.headers?.get('x-error') || `Publish failed (${resp.status})`; + return { error: xError }; + } + return resp.json(); +} + +export async function createOverride(org, base, satellite, pagePath) { + const basePath = `${DA_ORIGIN}/source/${org}/${base}${pagePath}.html`; + const resp = await daFetch(basePath); + if (!resp.ok) return { error: `Failed to fetch base content (${resp.status})` }; + + const html = await resp.text(); + const blob = new Blob([html], { type: 'text/html' }); + const formData = new FormData(); + formData.append('data', blob); + + const satPath = `${DA_ORIGIN}/source/${org}/${satellite}${pagePath}.html`; + const saveResp = await daFetch(satPath, { method: 'PUT', body: formData }); + if (!saveResp.ok) return { error: `Failed to create override (${saveResp.status})` }; + return { ok: true }; +} + +export async function getSatellitePageStatus(org, satellite, pagePath) { + const aemPath = pagePath.replace('.html', ''); + const url = `${AEM_ADMIN}/status/${org}/${satellite}/main${aemPath}`; + const resp = await daFetch(url); + if (!resp.ok) return { preview: false, live: false }; + const json = await resp.json(); + return { + preview: json.preview?.status === 200, + live: json.live?.status === 200, + }; +} + +export async function deleteOverride(org, satellite, pagePath) { + const satPath = `${DA_ORIGIN}/source/${org}/${satellite}${pagePath}.html`; + const resp = await daFetch(satPath, { method: 'DELETE' }); + if (!resp.ok) return { error: `Failed to delete override (${resp.status})` }; + return { ok: true }; +} + +let mergeCopyFn; +export function setMergeCopy(fn) { mergeCopyFn = fn; } + +export async function mergeFromBase(org, base, satellite, pagePath) { + try { + const mergeCopy = mergeCopyFn + || (await import(`${getNx()}/blocks/loc/project/index.js`)).mergeCopy; + + const url = { + source: `/${org}/${base}${pagePath}.html`, + destination: `/${org}/${satellite}${pagePath}.html`, + }; + + const result = await mergeCopy(url, 'MSM Merge'); + if (!result?.ok) return { error: 'Merge failed' }; + + const editUrl = `${window.location.origin}/edit#/${org}/${satellite}${pagePath}`; + return { ok: true, editUrl }; + } catch (e) { + return { error: e.message || 'Merge failed' }; + } +} diff --git a/blocks/edit/da-prepare/actions/msm/msm.css b/blocks/edit/da-prepare/actions/msm/msm.css new file mode 100644 index 00000000..88498256 --- /dev/null +++ b/blocks/edit/da-prepare/actions/msm/msm.css @@ -0,0 +1,420 @@ +:host { + display: flex; + flex-direction: column; + gap: 16px; + width: 500px; + min-height: 280px; + margin: 0 24px 24px; + + p { margin: 0; } +} + +:host(:has(.picker-menu)) { + min-height: 320px; +} + +.loading, +.no-satellites { + font-size: 14px; + font-style: italic; + color: var(--s2-gray-600, #717171); +} + +/* --- Satellite status line --- */ + +.sat-status-line { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--s2-gray-800, #292929); +} + +.sat-status-label { + color: var(--s2-gray-600, #717171); +} + +.sat-status-value { + font-weight: 600; +} + +/* --- Action row (side-by-side dropdowns) --- */ + +.action-row { + display: flex; + align-items: flex-start; + gap: 16px; + + .form-row { + flex: 0 0 calc(50% - 8px); + min-width: 0; + } +} + +/* --- Form row (rollout-inspired) --- */ + +.form-row { + display: flex; + flex-direction: column; + gap: 4px; + + > label { + font-size: var(--s2-body-xs-size, 12px); + display: block; + color: rgb(80 80 80); + margin-bottom: 0; + } +} + +/* --- Picker trigger --- */ + +.picker-wrapper { + position: relative; +} + +.picker-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + height: 32px; + padding: 0 10px; + background: var(--s2-gray-75, #f8f8f8); + font-family: var(--font-family, "Adobe Clean", adobe-clean, "Trebuchet MS", sans-serif); + font-size: 14px; + color: var(--s2-gray-800, #292929); + border: 1px solid var(--s2-gray-300, #d1d1d1); + border-radius: 8px; + cursor: pointer; + box-sizing: border-box; + transition: border-color 0.15s, background-color 0.15s; + + &:hover:not(:disabled) { + border-color: var(--s2-gray-500, #929292); + } + + &.open { + border-color: var(--s2-blue-900, #3b63fb); + } + + &:focus-visible { + outline: 2px solid var(--s2-blue-900, #3b63fb); + outline-offset: 2px; + } + + &:disabled { + background: var(--s2-gray-75, #f3f3f3); + border-color: var(--s2-gray-200, #e1e1e1); + color: var(--s2-gray-400, #b8b8b8); + cursor: default; + } +} + +.picker-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.picker-chevron { + width: 10px; + height: 10px; + flex-shrink: 0; + color: var(--s2-gray-600, #717171); + transition: transform 0.15s; + + .open & { transform: rotate(180deg); } + :disabled & { color: var(--s2-gray-400, #b8b8b8); } +} + +/* --- Picker menu (floating overlay) --- */ + +.picker-menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + z-index: 10; + list-style: none; + margin: 0; + padding: 6px; + background: #fff; + border: 1px solid var(--s2-gray-200, #e1e1e1); + border-radius: 8px; + box-shadow: 0 4px 16px rgb(0 0 0 / 12%); +} + +.picker-group-header { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--s2-gray-500, #929292); + padding: 8px 10px 4px; + user-select: none; + + &:first-child { padding-top: 4px; } +} + +.picker-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + font-size: 14px; + color: var(--s2-gray-800, #292929); + border-radius: 6px; + cursor: pointer; + user-select: none; + transition: background-color 0.1s; + + &:hover { background: var(--s2-gray-100, #f5f5f5); } + + &.selected { + font-weight: 600; + + .picker-checkmark { visibility: visible; } + } +} + +.picker-checkmark { + width: 12px; + height: 12px; + flex-shrink: 0; + visibility: hidden; + color: var(--s2-gray-800, #292929); +} + +/* --- Two-column grid --- */ + +.satellite-grid { + display: flex; + gap: 16px; + margin-top: 16px; +} + +.satellite-column { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.column-heading { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--s2-gray-500, #929292); + padding-bottom: 4px; + border-bottom: 1px solid var(--s2-gray-200, #e1e1e1); + margin-bottom: 2px; +} + +/* --- Satellite list --- */ + +.satellite-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 260px; + overflow-y: auto; + + &::-webkit-scrollbar { width: 5px; } + &::-webkit-scrollbar-track { background: transparent; } + &::-webkit-scrollbar-thumb { + background: var(--s2-gray-300, #d1d1d1); + border-radius: 3px; + &:hover { background: var(--s2-gray-500, #999); } + } +} + +.sat-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 4px; + border-radius: 4px; + transition: background-color 0.1s; + + &:hover:not(.out-of-scope) { background: var(--s2-gray-100, #f5f5f5); } + + &.out-of-scope { + opacity: 0.38; + } + + label { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; + cursor: pointer; + font-size: 14px; + color: var(--s2-gray-900, #292929); + } + + label span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +/* --- S2 checkbox (matches spectrum-two.css tokens) --- */ + +.sat-row input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 14px; + height: 14px; + margin: 0; + border: 2px solid var(--s2-gray-600, #717171); + border-radius: 2px; + cursor: pointer; + flex-shrink: 0; + position: relative; + transition: border 0.13s ease-in-out; + + &:checked { + border-color: var(--s2-gray-800, #292929); + border-width: 7px; + background: var(--s2-gray-50, #f8f8f8); + + &::after { + content: ''; + position: absolute; + inset: 0; + top: -2px; + left: -3px; + /* checkmark: 2px white strokes */ + width: 4px; + height: 8px; + margin: auto; + border: solid var(--s2-gray-50, #f8f8f8); + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + } + + &:focus-visible { + outline: 2px solid var(--s2-blue-900, #3b63fb); + outline-offset: 2px; + } + + &:disabled { opacity: 0.4; cursor: default; } + + &:hover:not(:disabled) { + border-color: var(--s2-gray-700, #505050); + } + + &:checked:hover:not(:disabled) { + border-color: var(--s2-gray-800, #292929); + } +} + +/* --- Status icons --- */ + +.result-icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.result-icon.success { color: #0d6e31; } +.result-icon.error { color: var(--s2-red-900, #d31510); } +.result-icon.pending { + color: var(--s2-gray-600, #717171); + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* --- Icon button (open-in-editor) --- */ + +.icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex-shrink: 0; + border: none; + border-radius: 4px; + background: none; + color: var(--s2-gray-500, #929292); + cursor: pointer; + padding: 0; + text-decoration: none; + transition: background-color 0.15s, color 0.15s; + + svg { width: 14px; height: 14px; fill: currentColor; } + &:hover { + background: var(--s2-gray-200, #e1e1e1); + color: var(--s2-gray-900, #292929); + } +} + +/* --- Footer actions --- */ + +.form-actions { + display: flex; + justify-content: flex-end; +} + +/* --- Confirm dialog --- */ + +.confirm-box { + padding: 12px; + background: #fef9ee; + border: 1px solid #f0dca0; + border-radius: 8px; + font-size: 14px; + color: var(--s2-gray-900, #292929); + + p { margin: 0 0 10px; line-height: 1.4; } + + .confirm-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + } +} + +.confirm-btn { + appearance: none; + border: 1px solid var(--s2-gray-400, #d1d1d1); + border-radius: 4px; + background: #fff; + color: var(--s2-gray-800, #3e3e3e); + font-size: 13px; + font-weight: 500; + font-family: inherit; + padding: 5px 12px; + cursor: pointer; + white-space: nowrap; + transition: background-color 0.15s, border-color 0.15s; + + &:hover { + background: var(--s2-gray-100, #f5f5f5); + border-color: var(--s2-gray-500, #929292); + } + + &:focus-visible { + outline: 2px solid var(--s2-blue-900, #1473e6); + outline-offset: 2px; + } + + &.danger { + color: var(--s2-red-900, #d31510); + border-color: #f0c8c2; + &:hover { + background: #fef0ee; + border-color: var(--s2-red-900, #d31510); + } + } +} diff --git a/blocks/edit/da-prepare/actions/msm/msm.js b/blocks/edit/da-prepare/actions/msm/msm.js new file mode 100644 index 00000000..bb8449bb --- /dev/null +++ b/blocks/edit/da-prepare/actions/msm/msm.js @@ -0,0 +1,581 @@ +import { LitElement, html, nothing } from 'da-lit'; +import getSheet from '../../../../shared/sheet.js'; +import { getSatellites, getBaseSite, isPageLocal, checkOverrides } from './helpers/config.js'; +import { + previewSatellite, + publishSatellite, + createOverride, + deleteOverride, + mergeFromBase, + getSatellitePageStatus, +} from './helpers/utils.js'; + +const sheet = await getSheet(import.meta.url.replace('js', 'css')); + +const STATUS = { pending: 'pending', success: 'success', error: 'error' }; +const SYNC_MODE = { override: 'override', merge: 'merge' }; + +const ACTION_SCOPE = { + preview: 'inherited', + publish: 'inherited', + break: 'inherited', + sync: 'custom', + reset: 'custom', +}; + +class DaMsm extends LitElement { + static properties = { + details: { attribute: false }, + _satellites: { state: true }, + _selected: { state: true }, + _loading: { state: true }, + _busy: { state: true }, + _confirmAction: { state: true }, + _action: { state: true }, + _syncMode: { state: true }, + _role: { state: true }, + _baseSite: { state: true }, + _hasOverride: { state: true }, + _satStatus: { state: true }, + _openPicker: { state: true }, + }; + + connectedCallback() { + super.connectedCallback(); + this.shadowRoot.adoptedStyleSheets = [sheet]; + this._loading = 'Loading\u2026'; + this._selected = new Set(); + this._action = 'preview'; + this._syncMode = SYNC_MODE.merge; + this._busy = false; + this._openPicker = null; + this._handleOutsidePickerClick = this._handleOutsidePickerClick.bind(this); + this.loadSatellites(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener('pointerdown', this._handleOutsidePickerClick); + } + + _handleOutsidePickerClick(e) { + if (!e.composedPath().includes(this)) { + this._openPicker = null; + document.removeEventListener('pointerdown', this._handleOutsidePickerClick); + } + } + + togglePicker(name) { + if (this._openPicker === name) { + this._openPicker = null; + document.removeEventListener('pointerdown', this._handleOutsidePickerClick); + } else { + this._openPicker = name; + document.addEventListener('pointerdown', this._handleOutsidePickerClick); + } + } + + selectPickerOption(name, value, setter) { + setter(value); + this._openPicker = null; + document.removeEventListener('pointerdown', this._handleOutsidePickerClick); + } + + async loadSatellites() { + const { org, site, path } = this.details; + this._loading = 'Loading configuration\u2026'; + + const satellites = await getSatellites(org, site); + + if (satellites && Object.keys(satellites).length) { + this._role = 'base'; + this._loading = 'Checking overrides\u2026'; + const results = await checkOverrides(org, satellites, path); + this._satellites = results.map((sat) => ({ ...sat, status: undefined })); + this._loading = undefined; + return; + } + + const baseSite = await getBaseSite(org, site); + if (baseSite) { + this._role = 'satellite'; + this._baseSite = baseSite; + this._action = 'sync-from-base'; + this._hasOverride = await isPageLocal(org, site, path); + this._loading = undefined; + return; + } + + this._satellites = []; + this._loading = undefined; + } + + get _inherited() { + return this._satellites?.filter((s) => !s.hasOverride) || []; + } + + get _custom() { + return this._satellites?.filter((s) => s.hasOverride) || []; + } + + get _targets() { + const scope = ACTION_SCOPE[this._action]; + const pool = scope === 'custom' ? this._custom : this._inherited; + return pool.filter((s) => this._selected.has(s.site)); + } + + get _canApply() { + return !this._busy && this._targets.length > 0; + } + + handleToggle(site) { + const next = new Set(this._selected); + if (next.has(site)) next.delete(site); + else next.add(site); + this._selected = next; + } + + clearStatuses() { + this._satellites = this._satellites?.map((s) => ({ ...s, status: undefined })); + } + + updateSatStatus(site, status) { + this._satellites = this._satellites.map( + (s) => (s.site === site ? { ...s, status } : s), + ); + } + + async apply() { + if (this._role === 'satellite') { + this.applySatelliteAction(); + return; + } + + if (!this._canApply) return; + + if (this._action === 'reset') { + const names = this._targets.map((s) => s.label).join(', '); + this._confirmAction = { message: `Resume inheritance for ${names}? This deletes local overrides.` }; + return; + } + + await this.runAction(this._action); + } + + cancelConfirm() { + this._confirmAction = undefined; + } + + async doConfirmedAction() { + const { confirmedAction } = this._confirmAction || {}; + this._confirmAction = undefined; + if (confirmedAction === 'resume-inheritance') { + await this.runSatelliteAction('resume-inheritance'); + } else { + await this.runAction('reset'); + } + } + + async runAction(action) { + this._busy = true; + const { org, site, path } = this.details; + const targets = this._targets; + + targets.forEach((s) => this.updateSatStatus(s.site, STATUS.pending)); + + switch (action) { + case 'preview': + case 'publish': { + const fn = action === 'publish' ? publishSatellite : previewSatellite; + await Promise.allSettled(targets.map(async (sat) => { + const result = await fn(org, sat.site, path); + this.updateSatStatus(sat.site, result.error ? STATUS.error : STATUS.success); + })); + break; + } + + case 'break': + await Promise.allSettled(targets.map(async (sat) => { + const result = await createOverride(org, site, sat.site, path); + if (result.error) { + this.updateSatStatus(sat.site, STATUS.error); + } else { + this._satellites = this._satellites.map( + (s) => (s.site === sat.site + ? { ...s, hasOverride: true, status: STATUS.success } + : s), + ); + } + })); + break; + + case 'sync': + if (this._syncMode === SYNC_MODE.merge) { + await Promise.allSettled(targets.map(async (sat) => { + const result = await mergeFromBase(org, site, sat.site, path); + if (result.error) { + this.updateSatStatus(sat.site, STATUS.error); + } else { + this._satellites = this._satellites.map( + (s) => (s.site === sat.site + ? { ...s, editUrl: result.editUrl, status: STATUS.success } + : s), + ); + } + })); + } else { + await Promise.allSettled(targets.map(async (sat) => { + const result = await createOverride(org, site, sat.site, path); + this.updateSatStatus(sat.site, result.error ? STATUS.error : STATUS.success); + })); + } + break; + + case 'reset': + await Promise.allSettled(targets.map(async (sat) => { + const pageStatus = await getSatellitePageStatus(org, sat.site, path); + const result = await deleteOverride(org, sat.site, path); + if (result.error) { + this.updateSatStatus(sat.site, STATUS.error); + } else { + if (pageStatus.live) { + await previewSatellite(org, sat.site, path); + await publishSatellite(org, sat.site, path); + } else if (pageStatus.preview) { + await previewSatellite(org, sat.site, path); + } + this._satellites = this._satellites.map( + (s) => (s.site === sat.site + ? { ...s, hasOverride: false, status: STATUS.success } + : s), + ); + } + })); + break; + + default: + break; + } + + this._selected = new Set(); + this._busy = false; + } + + applySatelliteAction() { + if (this._busy) return; + + if (this._action === 'resume-inheritance') { + this._confirmAction = { + message: 'Resume inheritance? This deletes the local override.', + confirmedAction: 'resume-inheritance', + }; + return; + } + + this.runSatelliteAction(this._action); + } + + async runSatelliteAction(action) { + this._busy = true; + this._satStatus = STATUS.pending; + const { org, site, path } = this.details; + + try { + let result; + if (action === 'sync-from-base') { + result = this._syncMode === SYNC_MODE.merge + ? await mergeFromBase(org, this._baseSite, site, path) + : await createOverride(org, this._baseSite, site, path); + } else if (action === 'resume-inheritance') { + const pageStatus = await getSatellitePageStatus(org, site, path); + result = await deleteOverride(org, site, path); + if (!result?.error) { + if (pageStatus.live) { + await previewSatellite(org, site, path); + await publishSatellite(org, site, path); + } else if (pageStatus.preview) { + await previewSatellite(org, site, path); + } + } + } + + if (result?.error) { + this._satStatus = STATUS.error; + } else { + this._satStatus = STATUS.success; + if (action === 'resume-inheritance') { + this._hasOverride = false; + } else { + this._hasOverride = true; + } + } + } catch { + this._satStatus = STATUS.error; + } + + this._busy = false; + } + + renderStatusIcon(sat) { + if (!sat.status) return nothing; + if (sat.status === STATUS.pending) { + return html``; + } + if (sat.status === STATUS.success) { + return html``; + } + return html``; + } + + renderSatellite(sat) { + const scope = ACTION_SCOPE[this._action]; + const outOfScope = (scope === 'inherited') === sat.hasOverride; + + return html` +
${this._confirmAction.message}
+Inherited
+Custom
+${this._loading}
`; + } + + if (this._role === 'satellite') { + return this.renderSatelliteView(); + } + + if (!this._satellites?.length) { + return html`No satellite sites configured.
`; + } + + return html` + ${this.renderActionControls()} + ${this.renderList()} + ${this.renderConfirm()} +