Skip to content

Commit f690c99

Browse files
seanchoi0claude
andcommitted
fix(bulk-publish): polling termination, worker auth, and selection panel
- Fix indefinite bulk publish polling: worker was getting 404 on project fragment because masAccessToken (sessionStorage) must be used instead of the plain IMS token; forward aemOdinEndpoint through the dispatch chain so the worker hits the correct AEM environment - Prevent double-dispatch: move publishing store flag before first await in startPublishing so it acts as a synchronous mutex; add early-return guard when project is already in-flight - Skip hydrated references during status polls (skipReferences:true) to avoid repeated ?references=direct-hydrated requests while polling - Fail fast in worker when project has no fragment paths: set FAILED status immediately instead of silently returning published with 0 items - Selection panel: restore opt-in cascade publish dialog for multi-select and resolve fragment refs from field values instead of hydrated refs - Publish dialog: show fragment title and studio path instead of DAM path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ef3ebf0 commit f690c99

13 files changed

Lines changed: 146 additions & 20 deletions

io/studio/src/bulk-publish/bulk-publish-worker.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ async function runWorker(input, deps = {}) {
5252
const title = projTitle(fragment);
5353
const existingSnapshots = projSnapshots(fragment);
5454

55+
if (paths.length === 0 && !hasPendingSnapshot(existingSnapshots)) {
56+
await updateProject(odinEndpoint, projectId, authToken, {
57+
status: PROJECT_STATUS.FAILED,
58+
lastError: 'No fragments found in project',
59+
});
60+
logger.error(JSON.stringify({ event: 'worker-no-paths', projectId }));
61+
return {
62+
total: 0,
63+
published: 0,
64+
failed: 0,
65+
startedAt,
66+
finishedAt: now().toISOString(),
67+
reasons: {},
68+
failures: [],
69+
failuresTruncated: false,
70+
};
71+
}
72+
5573
let snapshotEntries;
5674
let expandedPaths = null;
5775
if (hasPendingSnapshot(existingSnapshots)) {

io/studio/src/bulk-publish/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async function main(params) {
2727
projectId: params.projectId,
2828
publishedBy: params.publishedBy || '',
2929
authToken,
30+
aemOdinEndpoint: odinEndpoint,
3031
includeCards: params.includeCards || false,
3132
includeVariations: params.includeVariations || false,
3233
},

io/studio/test/bulk-publish/bulk-publish-worker.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,21 @@ describe('bulk-publish-worker — runWorker', () => {
315315

316316
expect(deps.createSnapshot).to.have.been.calledOnce;
317317
});
318+
319+
it('sets status to Failed and returns early when project has no paths and no pending snapshot', async () => {
320+
const { PROJECT_STATUS } = require('../../src/bulk-publish/project.js');
321+
deps.getProjectPaths.returns([]);
322+
deps.getProjectSnapshots.returns([]);
323+
324+
const result = await worker.runWorker({ projectId: 'proj-1', odinEndpoint: 'https://odin', authToken: 't' }, deps);
325+
326+
expect(deps.createSnapshot).to.not.have.been.called;
327+
expect(deps.publishResolved).to.not.have.been.called;
328+
const updateCall = deps.updateProjectFragment.firstCall;
329+
expect(updateCall.args[3].status).to.equal(PROJECT_STATUS.FAILED);
330+
expect(updateCall.args[3].lastError).to.be.a('string').and.not.be.empty;
331+
expect(result.total).to.equal(0);
332+
});
318333
});
319334

320335
describe('bulk-publish-worker — terminalStatus', () => {

io/studio/test/bulk-publish/index.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('bulk-publish/index.js — dispatcher', () => {
3737
expect(params.projectId).to.equal('proj-1');
3838
expect(params.publishedBy).to.equal('u@x.com');
3939
expect(params.authToken).to.equal('token');
40-
expect(params).to.not.have.property('odinEndpoint');
40+
expect(params.aemOdinEndpoint).to.equal('https://odin');
4141
});
4242

4343
it('returns 400 and does not invoke worker when projectId missing', async () => {

studio/src/aem/aem.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ class AEM {
199199
* @param {AbortController} abortController used for cancellation
200200
* @returns {Promise<Object>} the raw fragment item
201201
*/
202-
async getFragmentById(baseUrl, id, headers, abortController) {
203-
const response = await fetch(`${baseUrl}/adobe/sites/cf/fragments/${id}?references=direct-hydrated`, {
202+
async getFragmentById(baseUrl, id, headers, abortController, { references = 'direct-hydrated' } = {}) {
203+
const refParam = references ? `?references=${references}` : '';
204+
const response = await fetch(`${baseUrl}/adobe/sites/cf/fragments/${id}${refParam}`, {
204205
headers,
205206
signal: abortController?.signal,
206207
});
@@ -1394,7 +1395,8 @@ class AEM {
13941395
/**
13951396
* @see AEM#getFragmentById
13961397
*/
1397-
getById: (id, abortController) => this.getFragmentById(this.baseUrl, id, this.headers, abortController),
1398+
getById: (id, abortController, options) =>
1399+
this.getFragmentById(this.baseUrl, id, this.headers, abortController, options),
13981400
/**
13991401
* @see AEM#getFragmentWithEtag
14001402
*/

studio/src/bulk-publish/bulk-publish-client.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,14 @@ export async function publishBulk({
4646
projectId,
4747
publishedBy = '',
4848
token,
49+
aemOdinEndpoint,
4950
includeVariations = false,
5051
includeCards = false,
5152
}) {
5253
if (!projectId) throw new BulkPublishError('projectId is required');
53-
return callAction(ioBaseUrl, ENDPOINT, { projectId, publishedBy, includeVariations, includeCards }, token);
54+
const payload = { projectId, publishedBy, includeVariations, includeCards };
55+
if (aemOdinEndpoint) payload.aemOdinEndpoint = aemOdinEndpoint;
56+
return callAction(ioBaseUrl, ENDPOINT, payload, token);
5457
}
5558

5659
export async function revertAction({ ioBaseUrl, projectId, token }) {

studio/src/bulk-publish/bulk-publish-store.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export async function startPublishing({
1818
project,
1919
token,
2020
ioBaseUrl,
21+
aemOdinEndpoint,
2122
repository,
2223
publishFn,
2324
pollIntervalMs = 2000,
@@ -26,24 +27,25 @@ export async function startPublishing({
2627
includeVariations = false,
2728
includeCards = false,
2829
}) {
29-
const fn = publishFn ?? (await import('./bulk-publish-client.js')).publishBulk;
30-
const profile = await window.adobeIMS?.getProfile?.().catch(() => null);
31-
const publishedBy = profile?.email ?? '';
32-
30+
if (Store.bulkPublishProjects.publishing.get()[project.id]) return { alreadyPublishing: true };
3331
Store.bulkPublishProjects.publishing.set({
3432
...Store.bulkPublishProjects.publishing.get(),
3533
[project.id]: true,
3634
});
35+
36+
const fn = publishFn ?? (await import('./bulk-publish-client.js')).publishBulk;
37+
const profile = await window.adobeIMS?.getProfile?.().catch(() => null);
38+
const publishedBy = profile?.email ?? '';
3739
const terminalStatuses = new Set([
3840
BULK_PUBLISH_STATUS.PUBLISHED,
3941
BULK_PUBLISH_STATUS.PARTIALLY_PUBLISHED,
4042
BULK_PUBLISH_STATUS.FAILED,
4143
]);
4244
try {
43-
await fn({ ioBaseUrl, projectId: project.id, publishedBy, token, includeVariations, includeCards });
45+
await fn({ ioBaseUrl, projectId: project.id, publishedBy, token, aemOdinEndpoint, includeVariations, includeCards });
4446
let interval = pollIntervalMs;
4547
for (let i = 0; i < maxPolls; i++) {
46-
await repository.refreshFragment(project, { skipPromoMerge: true }).catch(() => {});
48+
await repository.refreshFragment(project, { skipPromoMerge: true, skipReferences: true }).catch(() => {});
4749
const statusField = project.get()?.fields?.find((f) => f.name === 'status');
4850
const status = statusField?.values?.[0];
4951
if (terminalStatuses.has(status)) return { status };

studio/src/bulk-publish/mas-bulk-publish-editor.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ class MasBulkPublishEditor extends LitElement {
228228
}
229229

230230
get token() {
231-
return window.adobeIMS?.getAccessToken()?.token;
231+
return sessionStorage.getItem('masAccessToken') ?? window.adobeIMS?.getAccessToken()?.token;
232232
}
233233

234234
get ioBaseUrl() {
@@ -856,10 +856,12 @@ class MasBulkPublishEditor extends LitElement {
856856
try {
857857
const outcome = await this.#withPendingAction(QUICK_ACTION.PUBLISH, async () => {
858858
const { startPublishing } = await import('./bulk-publish-store.js');
859+
const aemBaseUrl = this.repository?.aem?.baseUrl;
859860
return startPublishing({
860861
project: this.project,
861862
token: this.token,
862863
ioBaseUrl: this.ioBaseUrl,
864+
aemOdinEndpoint: aemBaseUrl?.startsWith('http://localhost') ? undefined : aemBaseUrl,
863865
repository: this.repository,
864866
includeVariations,
865867
includeCards,

studio/src/mas-repository.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2211,10 +2211,10 @@ export class MasRepository extends LitElement {
22112211
* Updates a given fragment store with the latest data
22122212
* @param {FragmentStore} store
22132213
*/
2214-
async refreshFragment(store, { skipPromoMerge = false } = {}) {
2214+
async refreshFragment(store, { skipPromoMerge = false, skipReferences = false } = {}) {
22152215
store.setLoading(true);
22162216
const id = store.get().id;
2217-
let latest = await this.aem.sites.cf.fragments.getById(id);
2217+
let latest = await this.aem.sites.cf.fragments.getById(id, null, skipReferences ? { references: null } : undefined);
22182218
if (!skipPromoMerge) {
22192219
latest = await promotionsRepository.mergePromoReferencesIntoFragmentData(this.aem, latest, () =>
22202220
this.loadPromotions(),

studio/src/mas-selection-panel.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,44 @@ class MasSelectionPanel extends LitElement {
116116

117117
if (fragmentIds.length === 0) return;
118118

119+
const allVariations = [];
120+
const allCards = [];
121+
const seen = new Set();
122+
123+
const hydratedFragments = await Promise.all(
124+
fragmentIds.map((id) => this.repository.aem.sites.cf.fragments.getById(id).catch(() => null)),
125+
);
126+
127+
for (const fragmentData of hydratedFragments) {
128+
if (!fragmentData) continue;
129+
const variationPaths = new Set(fragmentData.fields?.find((f) => f.name === 'variations')?.values ?? []);
130+
const cardPaths = new Set([
131+
...(fragmentData.fields?.find((f) => f.name === 'cards')?.values ?? []),
132+
...(fragmentData.fields?.find((f) => f.name === 'collections')?.values ?? []),
133+
]);
134+
for (const ref of fragmentData.references || []) {
135+
if (!ref?.id || seen.has(ref.id)) continue;
136+
if (variationPaths.has(ref.path)) {
137+
seen.add(ref.id);
138+
allVariations.push(ref);
139+
} else if (cardPaths.has(ref.path)) {
140+
seen.add(ref.id);
141+
allCards.push(ref);
142+
}
143+
}
144+
}
145+
146+
if (allVariations.length || allCards.length) {
147+
const { MasPublishDialog } = await import('./publish/mas-publish-dialog.js');
148+
const result = await MasPublishDialog.show({ variations: allVariations, cards: allCards });
149+
if (!result.confirmed) return;
150+
for (const id of result.selectedIds) {
151+
if (!fragmentIds.includes(id)) fragmentIds.push(id);
152+
}
153+
}
154+
119155
const success = await this.repository.bulkPublishFragments(fragmentIds);
120156
if (success) {
121-
// Clear selection after successful publish
122157
this.selectionStore.set([]);
123158
}
124159
}

0 commit comments

Comments
 (0)