diff --git a/blocks/browse/da-actionbar/da-actionbar.js b/blocks/browse/da-actionbar/da-actionbar.js index e0b830ce8..b15820afb 100644 --- a/blocks/browse/da-actionbar/da-actionbar.js +++ b/blocks/browse/da-actionbar/da-actionbar.js @@ -28,6 +28,7 @@ export default class DaActionBar extends LitElement { loading: { attribute: false }, isFavorite: { attribute: false }, isHlx6: { attribute: false }, + hidePublishConfs: { attribute: false }, _isCopying: { state: true }, _isDeleting: { state: true }, _isMoving: { state: true }, @@ -39,6 +40,7 @@ export default class DaActionBar extends LitElement { this.items = []; this.currentPath = ''; this.isFavorite = false; + this.hidePublishConfs = []; this.canDelete = true; } @@ -143,6 +145,16 @@ export default class DaActionBar extends LitElement { return this._canWrite && this.items.some((item) => item.ext && item.ext !== 'link') && !this._isCopying; } + get _hidePublish() { + return this.hidePublishConfs.some( + (prefix) => this.items.some((item) => item.path?.startsWith(prefix)), + ); + } + + get _canPublish() { + return this._canAemAction && !this._hidePublish; + } + get _canRename() { if (!this._canWrite) return false; const isFolder = !this.items[0]?.ext; @@ -232,7 +244,7 @@ export default class DaActionBar extends LitElement { diff --git a/blocks/browse/da-browse/da-browse.js b/blocks/browse/da-browse/da-browse.js index 1096b0214..39aca9e87 100644 --- a/blocks/browse/da-browse/da-browse.js +++ b/blocks/browse/da-browse/da-browse.js @@ -166,6 +166,10 @@ export default class DaBrowse extends LitElement { if (row.key === 'editor.path') acc.push(row.value); return acc; }, []); + this.hidePublishConfs = rows.reduce((acc, row) => { + if (row.key === 'editor.hidePublish') acc.push(row.value); + return acc; + }, []); } if (!this.editorConfs || this.editorConfs.length === 0) return DEF_EDIT; @@ -236,6 +240,7 @@ export default class DaBrowse extends LitElement { class="da-list-type-${type}" fullpath="${fullpath}" editor="${this.editor}" + .hidePublishConfs=${this.hidePublishConfs} @onpermissions=${this.handlePermissions} @selectionchanged=${type === 'browse' && this._chatEnabled ? this._handleBrowseSelection : nothing} select="${select ? true : nothing}" diff --git a/blocks/browse/da-list/da-list.js b/blocks/browse/da-list/da-list.js index 0f78b18a2..cd4fb13f0 100644 --- a/blocks/browse/da-list/da-list.js +++ b/blocks/browse/da-list/da-list.js @@ -24,6 +24,7 @@ export default class DaList extends LitElement { listtype: { type: String }, fullpath: { type: String }, editor: { type: String }, + hidePublishConfs: { attribute: false }, select: { type: Boolean }, sort: { type: Boolean }, drag: { type: Boolean }, @@ -1300,6 +1301,7 @@ export default class DaList extends LitElement { @onpublish=${this.handlePublish} @onshare=${this.handleShare} .loading=${typeof this._aemActionState === 'string' ? this._aemActionState : null} + .hidePublishConfs=${this.hidePublishConfs} currentPath="${this.fullpath}" .isHlx6=${this._isHlx6 ?? false} role="row" diff --git a/test/unit/blocks/browse/da-actionbar/da-actionbar.test.js b/test/unit/blocks/browse/da-actionbar/da-actionbar.test.js index 97dff709d..39e391b82 100644 --- a/test/unit/blocks/browse/da-actionbar/da-actionbar.test.js +++ b/test/unit/blocks/browse/da-actionbar/da-actionbar.test.js @@ -147,6 +147,44 @@ describe('DaActionBar', () => { }); }); + describe('_hidePublish / _canPublish', () => { + it('_hidePublish is false when hidePublishConfs is empty', () => { + const el = new DaActionBar(); + el.items = [{ ext: 'html', path: '/org/site/blog/page.html' }]; + expect(el._hidePublish).to.be.false; + }); + + it('_hidePublish is true when a selected item path matches a prefix', () => { + const el = new DaActionBar(); + el.items = [{ ext: 'html', path: '/org/site/blog/page.html' }]; + el.hidePublishConfs = ['/org/site/blog']; + expect(el._hidePublish).to.be.true; + }); + + it('_hidePublish is false when no selected item path matches any prefix', () => { + const el = new DaActionBar(); + el.items = [{ ext: 'html', path: '/org/site/news/page.html' }]; + el.hidePublishConfs = ['/org/site/blog']; + expect(el._hidePublish).to.be.false; + }); + + it('_canPublish is false when hidden, even with write permission', () => { + const el = new DaActionBar(); + el.permissions = ['read', 'write']; + el.items = [{ ext: 'html', path: '/org/site/blog/page.html' }]; + el.hidePublishConfs = ['/org/site/blog']; + expect(el._canPublish).to.be.false; + }); + + it('_canPublish is true when not hidden and _canAemAction is true', () => { + const el = new DaActionBar(); + el.permissions = ['read', 'write']; + el.items = [{ ext: 'html', path: '/org/site/news/page.html' }]; + el.hidePublishConfs = ['/org/site/blog']; + expect(el._canPublish).to.be.true; + }); + }); + describe('_canShare', () => { it('Returns false when no items have a non-link extension', () => { const el = new DaActionBar(); diff --git a/test/unit/blocks/browse/da-browse/da-browse.test.js b/test/unit/blocks/browse/da-browse/da-browse.test.js index b1076cd49..b05bd11b9 100644 --- a/test/unit/blocks/browse/da-browse/da-browse.test.js +++ b/test/unit/blocks/browse/da-browse/da-browse.test.js @@ -400,6 +400,38 @@ describe('DaBrowse Component', () => { const url = await daBrowseComp.getEditor(true); expect(url).to.equal('/edit#'); }); + + // Scope mocks to the site-level config URL only (org-level gets an empty + // config) since fetchDaConfigs fetches org + site in parallel, and a + // catch-all mock would return the same rows for both, doubling entries. + function mockSiteConfig(fullpath, json) { + window.fetch = async (url) => { + // getNx2Api's config.get pings isHlx6 first (HLX_ADMIN/ping/{org}/{site}); answer that + // with a real Response (so its headers.get() call is safe) and defer everything else. + if (String(url).includes('/ping/')) return new Response('', { status: 200 }); + if (url.includes(`/config${fullpath}/`)) return { ok: true, json: async () => json }; + return { ok: true, json: async () => ({ data: [] }) }; + }; + } + + it('collects editor.hidePublish rows into hidePublishConfs', async () => { + daBrowseComp.details = { fullpath: '/myorg-d/mysite/folder', org: 'myorg-d', site: 'mysite', owner: 'myorg-d', depth: 3 }; + mockSiteConfig('/myorg-d/mysite', { + data: [ + { key: 'editor.path', value: '/myorg-d/mysite=https://da.live/form#' }, + { key: 'editor.hidePublish', value: '/myorg-d/mysite/blog' }, + ], + }); + await daBrowseComp.getEditor(true); + expect(daBrowseComp.hidePublishConfs).to.deep.equal(['/myorg-d/mysite/blog']); + }); + + it('returns an empty hidePublishConfs when no editor.hidePublish rows exist', async () => { + daBrowseComp.details = { fullpath: '/myorg-f/mysite/folder', org: 'myorg-f', site: 'mysite', owner: 'myorg-f', depth: 3 }; + mockSiteConfig('/myorg-f/mysite', { data: [{ key: 'editor.path', value: '/myorg-f/mysite=https://da.live/form#' }] }); + await daBrowseComp.getEditor(true); + expect(daBrowseComp.hidePublishConfs).to.deep.equal([]); + }); }); describe('isRootFolder', () => { diff --git a/test/unit/blocks/browse/da-list/da-list-render.test.js b/test/unit/blocks/browse/da-list/da-list-render.test.js index 9f25fdbc3..307e52d16 100644 --- a/test/unit/blocks/browse/da-list/da-list-render.test.js +++ b/test/unit/blocks/browse/da-list/da-list-render.test.js @@ -182,6 +182,13 @@ describe('da-list render', () => { const bar = el.shadowRoot.querySelector('da-actionbar'); expect(bar.getAttribute('data-visible')).to.equal('true'); }); + + it('Forwards hidePublishConfs to the action bar', async () => { + await fixture({ fullpath: '/o/r', hidePublishConfs: ['/o/r/blog'] }); + await rerender(); + const bar = el.shadowRoot.querySelector('da-actionbar'); + expect(bar.hidePublishConfs).to.deep.equal(['/o/r/blog']); + }); }); describe('da-list pagination observer', () => { diff --git a/test/unit/blocks/edit/prose/index.test.js b/test/unit/blocks/edit/prose/index.test.js index ab31501e3..0add80df4 100644 --- a/test/unit/blocks/edit/prose/index.test.js +++ b/test/unit/blocks/edit/prose/index.test.js @@ -31,6 +31,21 @@ const stubHlx6Ping = () => { return () => { window.fetch = saved; }; }; +// createConnection opens a real y-websocket connection to admin.da.live. Left +// unstubbed, the real socket's own close (e.g. a genuine 401 from the actual +// server) can race the test's synthetic 'connection-close' emit and double-count +// handler side effects (e.g. refreshToken calls). Stub WebSocket so no real +// socket is ever opened. Returns a restore fn. +const stubWebSocket = () => { + const saved = window.WebSocket; + window.WebSocket = function FakeWebSocket() { + this.readyState = 0; + this.close = () => {}; + this.send = () => {}; + }; + return () => { window.WebSocket = saved; }; +}; + function buildFakeWsProvider({ withSynced = false } = {}) { const listeners = new Map(); const winListeners = []; @@ -81,12 +96,15 @@ function buildFakeWsProvider({ withSynced = false } = {}) { describe('prose/index createConnection', () => { let restoreFetch; + let restoreWebSocket; beforeEach(() => { restoreFetch = stubHlx6Ping(); + restoreWebSocket = stubWebSocket(); window.localStorage.removeItem('nx-ims'); }); afterEach(() => { restoreFetch(); + restoreWebSocket(); // Always remove rather than restoring a prior value — if a leak entered // this block, restoring it would propagate the leak to later test files. window.localStorage.removeItem('nx-ims');