From f2c8bdc309d5ccce658c7b89e49063a721cdd95d Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Thu, 13 Aug 2026 09:07:39 -0700 Subject: [PATCH 1/4] feat(ew-actions): hide Publish per DA editor.hidePublish config Mirrors da-live's da-title.js filterActions(): fetches org/site DA config docs on hash change, ORs any editor.hidePublish rows, and drops publish from the Send button's menu items when the current document's fullpath starts with a configured prefix. Co-Authored-By: Claude Sonnet 5 --- nx2/blocks/ew-actions/ew-actions.js | 23 ++- test/nx2/blocks/ew-actions/ew-actions.test.js | 131 ++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 test/nx2/blocks/ew-actions/ew-actions.test.js diff --git a/nx2/blocks/ew-actions/ew-actions.js b/nx2/blocks/ew-actions/ew-actions.js index 268ff8a3b..6e93c3f65 100644 --- a/nx2/blocks/ew-actions/ew-actions.js +++ b/nx2/blocks/ew-actions/ew-actions.js @@ -7,6 +7,7 @@ import { runAemPreviewOrPublish, } from '../../utils/aem-preview-publish.js'; import { versions } from '../../utils/api.js'; +import { fetchDaConfigs, getFirstSheet } from '../../utils/daConfig.js'; import { getConfig } from '../../scripts/nx.js'; import '../shared/menu/menu.js'; @@ -56,6 +57,7 @@ class NXEwActions extends LitElement { _busy: { state: true }, _hasError: { state: true }, _hashState: { state: true }, + _hidePublish: { state: true }, _prepareReady: { state: true }, // phase: 'error' | 'pending' | 'result' _dialog: { state: true }, @@ -97,6 +99,25 @@ class NXEwActions extends LitElement { this._unsubHash?.(); } + update(changed) { + super.update(changed); + if (changed.has('_hashState') && this._hashState) this._filterHidePublish(); + } + + async _filterHidePublish() { + const { org, site } = this._hashState || {}; + const fullpath = buildPrepareDetails(this._hashState)?.fullpath; + if (!org || !site || !fullpath) { + this._hidePublish = false; + return; + } + + const configs = await Promise.all(fetchDaConfigs({ org, site })); + const configTab = configs.flatMap((config) => getFirstSheet(config) || []); + const publishConfigs = configTab.filter((c) => c.key === 'editor.hidePublish'); + this._hidePublish = publishConfigs.some((c) => fullpath.startsWith(c.value)); + } + _togglePrepareMenu(e) { e.preventDefault(); const btn = this._prepareBtn; @@ -269,7 +290,7 @@ class NXEwActions extends LitElement { size="m" .items=${[ { id: 'preview', label: 'Preview' }, - { id: 'publish', label: 'Publish' }, + ...(this._hidePublish ? [] : [{ id: 'publish', label: 'Publish' }]), ]} @select=${(e) => this._pickAem(e.detail.id)} > diff --git a/test/nx2/blocks/ew-actions/ew-actions.test.js b/test/nx2/blocks/ew-actions/ew-actions.test.js new file mode 100644 index 000000000..5188f0894 --- /dev/null +++ b/test/nx2/blocks/ew-actions/ew-actions.test.js @@ -0,0 +1,131 @@ +import { expect } from '@esm-bundle/chai'; +import { DA_ADMIN } from '../../../../nx2/utils/utils.js'; +import '../../../../nx2/blocks/ew-actions/ew-actions.js'; + +let seq = 0; +// Unique org/site per test avoids collisions with daConfig.js's module-level fetch cache. +function uniq(prefix) { + seq += 1; + return `${prefix}${Date.now()}${seq}`; +} + +function installFetch(responsesByUrlSubstring) { + const origFetch = window.fetch; + // Sort longest-key-first: the org-level config URL is a substring of the + // site-level one, so a naive first-match would always serve the org config. + const entries = Object.entries(responsesByUrlSubstring).sort(([a], [b]) => b.length - a.length); + window.fetch = async (url, opts) => { + const match = entries.find(([key]) => url.includes(key)); + if (match) return new Response(JSON.stringify(match[1]), { status: 200 }); + return origFetch(url, opts); + }; + return () => { window.fetch = origFetch; }; +} + +async function makeEl() { + const el = document.createElement('nx-ew-actions'); + document.body.append(el); + await el.updateComplete; + return el; +} + +describe('nx-ew-actions', () => { + let el; + let restoreFetch; + + afterEach(() => { + el?.remove(); + restoreFetch?.(); + }); + + describe('_filterHidePublish', () => { + it('does not hide publish when there is no open document', async () => { + el = await makeEl(); + el._hashState = null; + await el._filterHidePublish(); + expect(el._hidePublish).to.be.false; + }); + + it('hides publish when a matching editor.hidePublish config exists', async () => { + const org = uniq('org'); + const site = uniq('site'); + restoreFetch = installFetch({ + [`${DA_ADMIN}/config/${org}/`]: { data: [{ key: 'editor.hidePublish', value: `/${org}/${site}/test` }] }, + [`${DA_ADMIN}/config/${org}/${site}/`]: { data: [] }, + }); + + el = await makeEl(); + el._hashState = { org, site, path: '/test/page' }; + await el._filterHidePublish(); + + expect(el._hidePublish).to.be.true; + }); + + it('keeps publish when the editor.hidePublish config does not match the path', async () => { + const org = uniq('org'); + const site = uniq('site'); + restoreFetch = installFetch({ + [`${DA_ADMIN}/config/${org}/`]: { data: [{ key: 'editor.hidePublish', value: `/${org}/${site}/other` }] }, + [`${DA_ADMIN}/config/${org}/${site}/`]: { data: [] }, + }); + + el = await makeEl(); + el._hashState = { org, site, path: '/test/page' }; + await el._filterHidePublish(); + + expect(el._hidePublish).to.be.false; + }); + + it('ORs editor.hidePublish rows across org- and site-level configs', async () => { + const org = uniq('org'); + const site = uniq('site'); + restoreFetch = installFetch({ + [`${DA_ADMIN}/config/${org}/`]: { data: [{ key: 'editor.hidePublish', value: `/${org}/${site}/other` }] }, + [`${DA_ADMIN}/config/${org}/${site}/`]: { data: [{ key: 'editor.hidePublish', value: `/${org}/${site}/test` }] }, + }); + + el = await makeEl(); + el._hashState = { org, site, path: '/test/page' }; + await el._filterHidePublish(); + + expect(el._hidePublish).to.be.true; + }); + }); + + describe('render', () => { + it('includes both preview and publish menu items when publish is not hidden', async () => { + const org = uniq('org'); + const site = uniq('site'); + restoreFetch = installFetch({ + [`${DA_ADMIN}/config/${org}/`]: { data: [] }, + [`${DA_ADMIN}/config/${org}/${site}/`]: { data: [] }, + }); + + el = await makeEl(); + el._hashState = { org, site, path: '/test/page' }; + await el._filterHidePublish(); + await el.updateComplete; + + const ids = el.shadowRoot.querySelector('nx-menu').items.map((i) => i.id); + expect(ids).to.include.members(['preview', 'publish']); + }); + + it('omits the publish menu item (keeps preview) when publish is hidden', async () => { + const org = uniq('org'); + const site = uniq('site'); + restoreFetch = installFetch({ + [`${DA_ADMIN}/config/${org}/`]: { data: [{ key: 'editor.hidePublish', value: `/${org}/${site}/test` }] }, + [`${DA_ADMIN}/config/${org}/${site}/`]: { data: [] }, + }); + + el = await makeEl(); + el._hashState = { org, site, path: '/test/page' }; + await el._filterHidePublish(); + await el.updateComplete; + + const ids = el.shadowRoot.querySelector('nx-menu').items.map((i) => i.id); + expect(ids).to.include('preview'); + expect(ids).to.not.include('publish'); + }); + }); +}); From 2ef0f405e7982bafdd37b532923a61ccaf24a2bc Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Thu, 13 Aug 2026 10:47:08 -0700 Subject: [PATCH 2/4] fix(ew-actions): read editor.hidePublish from the "data" sheet by name getFirstSheet grabs whichever sheet is positionally first, which silently breaks if a config doc has multiple sheets and "data" isn't first. Look up the "data" sheet by name and only fall back to positional lookup if it doesn't exist. Co-Authored-By: Claude --- nx2/blocks/ew-actions/ew-actions.js | 5 +++-- nx2/utils/daConfig.js | 8 ++++++++ test/nx2/blocks/ew-actions/ew-actions.test.js | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/nx2/blocks/ew-actions/ew-actions.js b/nx2/blocks/ew-actions/ew-actions.js index 6e93c3f65..c0480cf7e 100644 --- a/nx2/blocks/ew-actions/ew-actions.js +++ b/nx2/blocks/ew-actions/ew-actions.js @@ -7,7 +7,7 @@ import { runAemPreviewOrPublish, } from '../../utils/aem-preview-publish.js'; import { versions } from '../../utils/api.js'; -import { fetchDaConfigs, getFirstSheet } from '../../utils/daConfig.js'; +import { fetchDaConfigs, getSheetByName, getFirstSheet } from '../../utils/daConfig.js'; import { getConfig } from '../../scripts/nx.js'; import '../shared/menu/menu.js'; @@ -113,7 +113,8 @@ class NXEwActions extends LitElement { } const configs = await Promise.all(fetchDaConfigs({ org, site })); - const configTab = configs.flatMap((config) => getFirstSheet(config) || []); + const configTab = configs.filter(Boolean) + .flatMap((config) => getSheetByName(config, 'data') ?? getFirstSheet(config) ?? []); const publishConfigs = configTab.filter((c) => c.key === 'editor.hidePublish'); this._hidePublish = publishConfigs.some((c) => fullpath.startsWith(c.value)); } diff --git a/nx2/utils/daConfig.js b/nx2/utils/daConfig.js index 40dbb5838..f62c01c70 100644 --- a/nx2/utils/daConfig.js +++ b/nx2/utils/daConfig.js @@ -7,6 +7,14 @@ export function getFirstSheet(json) { return json[Object.keys(json)[0]]?.data; } +/** Returns the named sheet's data array from a DA config JSON response (handles multi-sheet). */ +export function getSheetByName(json, name) { + if (json[':type'] !== 'multi-sheet') { + return json[':sheetname'] === name ? json.data : undefined; + } + return json[name]?.data; +} + /** Memoized fetches for `/{org}` and optional `/{org}/{site}` config documents. */ export const fetchDaConfigs = (() => { const cache = {}; diff --git a/test/nx2/blocks/ew-actions/ew-actions.test.js b/test/nx2/blocks/ew-actions/ew-actions.test.js index 5188f0894..bbfc26e4d 100644 --- a/test/nx2/blocks/ew-actions/ew-actions.test.js +++ b/test/nx2/blocks/ew-actions/ew-actions.test.js @@ -90,6 +90,26 @@ describe('nx-ew-actions', () => { expect(el._hidePublish).to.be.true; }); + + it('hides publish when editor.hidePublish lives in a non-first multi-sheet tab named "data"', async () => { + const org = uniq('org'); + const site = uniq('site'); + restoreFetch = installFetch({ + [`${DA_ADMIN}/config/${org}/`]: { + permissions: { data: [{ path: '/', groups: 'everyone', actions: 'write' }] }, + data: { data: [{ key: 'editor.hidePublish', value: `/${org}/${site}/test` }] }, + ':names': ['permissions', 'data'], + ':type': 'multi-sheet', + }, + [`${DA_ADMIN}/config/${org}/${site}/`]: { data: [] }, + }); + + el = await makeEl(); + el._hashState = { org, site, path: '/test/page' }; + await el._filterHidePublish(); + + expect(el._hidePublish).to.be.true; + }); }); describe('render', () => { From 7af5a8849c9b317971f2dd2c0a6c23a925bac464 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Tue, 1 Sep 2026 12:33:26 -0700 Subject: [PATCH 3/4] revert(ew-actions): drop "data" sheet-by-name lookup for editor.hidePublish Reverts the getSheetByName fallback back to plain getFirstSheet; better multi-sheet config lookup will be tackled separately. The editor.hidePublish feature itself is unchanged. Co-Authored-By: Claude Sonnet 5 --- nx2/blocks/ew-actions/ew-actions.js | 5 ++--- nx2/utils/daConfig.js | 8 -------- test/nx2/blocks/ew-actions/ew-actions.test.js | 20 ------------------- 3 files changed, 2 insertions(+), 31 deletions(-) diff --git a/nx2/blocks/ew-actions/ew-actions.js b/nx2/blocks/ew-actions/ew-actions.js index c0480cf7e..6e93c3f65 100644 --- a/nx2/blocks/ew-actions/ew-actions.js +++ b/nx2/blocks/ew-actions/ew-actions.js @@ -7,7 +7,7 @@ import { runAemPreviewOrPublish, } from '../../utils/aem-preview-publish.js'; import { versions } from '../../utils/api.js'; -import { fetchDaConfigs, getSheetByName, getFirstSheet } from '../../utils/daConfig.js'; +import { fetchDaConfigs, getFirstSheet } from '../../utils/daConfig.js'; import { getConfig } from '../../scripts/nx.js'; import '../shared/menu/menu.js'; @@ -113,8 +113,7 @@ class NXEwActions extends LitElement { } const configs = await Promise.all(fetchDaConfigs({ org, site })); - const configTab = configs.filter(Boolean) - .flatMap((config) => getSheetByName(config, 'data') ?? getFirstSheet(config) ?? []); + const configTab = configs.flatMap((config) => getFirstSheet(config) || []); const publishConfigs = configTab.filter((c) => c.key === 'editor.hidePublish'); this._hidePublish = publishConfigs.some((c) => fullpath.startsWith(c.value)); } diff --git a/nx2/utils/daConfig.js b/nx2/utils/daConfig.js index e88393030..934e90d87 100644 --- a/nx2/utils/daConfig.js +++ b/nx2/utils/daConfig.js @@ -6,14 +6,6 @@ export function getFirstSheet(json) { return json[json[':names']?.[0]]?.data; } -/** Returns the named sheet's data array from a DA config JSON response (handles multi-sheet). */ -export function getSheetByName(json, name) { - if (json[':type'] !== 'multi-sheet') { - return json[':sheetname'] === name ? json.data : undefined; - } - return json[name]?.data; -} - /** Memoized fetches for `/{org}` and optional `/{org}/{site}` config documents. */ export const fetchDaConfigs = (() => { const cache = {}; diff --git a/test/nx2/blocks/ew-actions/ew-actions.test.js b/test/nx2/blocks/ew-actions/ew-actions.test.js index bbfc26e4d..5188f0894 100644 --- a/test/nx2/blocks/ew-actions/ew-actions.test.js +++ b/test/nx2/blocks/ew-actions/ew-actions.test.js @@ -90,26 +90,6 @@ describe('nx-ew-actions', () => { expect(el._hidePublish).to.be.true; }); - - it('hides publish when editor.hidePublish lives in a non-first multi-sheet tab named "data"', async () => { - const org = uniq('org'); - const site = uniq('site'); - restoreFetch = installFetch({ - [`${DA_ADMIN}/config/${org}/`]: { - permissions: { data: [{ path: '/', groups: 'everyone', actions: 'write' }] }, - data: { data: [{ key: 'editor.hidePublish', value: `/${org}/${site}/test` }] }, - ':names': ['permissions', 'data'], - ':type': 'multi-sheet', - }, - [`${DA_ADMIN}/config/${org}/${site}/`]: { data: [] }, - }); - - el = await makeEl(); - el._hashState = { org, site, path: '/test/page' }; - await el._filterHidePublish(); - - expect(el._hidePublish).to.be.true; - }); }); describe('render', () => { From b37ad55fd01e54a475f302ebea73a144018f30b3 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Sat, 5 Sep 2026 18:31:57 -0700 Subject: [PATCH 4/4] fix(ew-actions): address PR review feedback on hide-publish check Wrap config fetch in try/catch, exclude empty editor.hidePublish values, and extract a pure shouldHidePublish helper. Co-Authored-By: Claude --- nx2/blocks/ew-actions/ew-actions.js | 31 +++++++++++-------- test/nx2/blocks/ew-actions/ew-actions.test.js | 14 ++++----- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/nx2/blocks/ew-actions/ew-actions.js b/nx2/blocks/ew-actions/ew-actions.js index 6e93c3f65..22528ec17 100644 --- a/nx2/blocks/ew-actions/ew-actions.js +++ b/nx2/blocks/ew-actions/ew-actions.js @@ -52,6 +52,21 @@ function buildPrepareDetails(state) { }; } +async function shouldHidePublish(hashState) { + const { org, site } = hashState || {}; + const fullpath = buildPrepareDetails(hashState)?.fullpath; + if (!org || !site || !fullpath) return false; + + try { + const configs = await Promise.all(fetchDaConfigs({ org, site })); + const configTab = configs.flatMap((config) => getFirstSheet(config) || []); + const publishConfigs = configTab.filter((c) => c.key === 'editor.hidePublish' && c.value); + return publishConfigs.some((c) => fullpath.startsWith(c.value)); + } catch { + return false; + } +} + class NXEwActions extends LitElement { static properties = { _busy: { state: true }, @@ -101,21 +116,11 @@ class NXEwActions extends LitElement { update(changed) { super.update(changed); - if (changed.has('_hashState') && this._hashState) this._filterHidePublish(); + if (changed.has('_hashState') && this._hashState) this._updateHidePublish(); } - async _filterHidePublish() { - const { org, site } = this._hashState || {}; - const fullpath = buildPrepareDetails(this._hashState)?.fullpath; - if (!org || !site || !fullpath) { - this._hidePublish = false; - return; - } - - const configs = await Promise.all(fetchDaConfigs({ org, site })); - const configTab = configs.flatMap((config) => getFirstSheet(config) || []); - const publishConfigs = configTab.filter((c) => c.key === 'editor.hidePublish'); - this._hidePublish = publishConfigs.some((c) => fullpath.startsWith(c.value)); + async _updateHidePublish() { + this._hidePublish = await shouldHidePublish(this._hashState); } _togglePrepareMenu(e) { diff --git a/test/nx2/blocks/ew-actions/ew-actions.test.js b/test/nx2/blocks/ew-actions/ew-actions.test.js index 5188f0894..94538eb32 100644 --- a/test/nx2/blocks/ew-actions/ew-actions.test.js +++ b/test/nx2/blocks/ew-actions/ew-actions.test.js @@ -38,11 +38,11 @@ describe('nx-ew-actions', () => { restoreFetch?.(); }); - describe('_filterHidePublish', () => { + describe('_updateHidePublish', () => { it('does not hide publish when there is no open document', async () => { el = await makeEl(); el._hashState = null; - await el._filterHidePublish(); + await el._updateHidePublish(); expect(el._hidePublish).to.be.false; }); @@ -56,7 +56,7 @@ describe('nx-ew-actions', () => { el = await makeEl(); el._hashState = { org, site, path: '/test/page' }; - await el._filterHidePublish(); + await el._updateHidePublish(); expect(el._hidePublish).to.be.true; }); @@ -71,7 +71,7 @@ describe('nx-ew-actions', () => { el = await makeEl(); el._hashState = { org, site, path: '/test/page' }; - await el._filterHidePublish(); + await el._updateHidePublish(); expect(el._hidePublish).to.be.false; }); @@ -86,7 +86,7 @@ describe('nx-ew-actions', () => { el = await makeEl(); el._hashState = { org, site, path: '/test/page' }; - await el._filterHidePublish(); + await el._updateHidePublish(); expect(el._hidePublish).to.be.true; }); @@ -103,7 +103,7 @@ describe('nx-ew-actions', () => { el = await makeEl(); el._hashState = { org, site, path: '/test/page' }; - await el._filterHidePublish(); + await el._updateHidePublish(); await el.updateComplete; const ids = el.shadowRoot.querySelector('nx-menu').items.map((i) => i.id); @@ -120,7 +120,7 @@ describe('nx-ew-actions', () => { el = await makeEl(); el._hashState = { org, site, path: '/test/page' }; - await el._filterHidePublish(); + await el._updateHidePublish(); await el.updateComplete; const ids = el.shadowRoot.querySelector('nx-menu').items.map((i) => i.id);