Skip to content

Commit 224efbd

Browse files
committed
Merge remote-tracking branch 'origin/feature/vsc-library-explorer' into feature/vsc-ext-native-improvements
# Conflicts: # packages/b2c-vs-extension/package.json # packages/b2c-vs-extension/src/extension.ts
2 parents 2608755 + ed309b1 commit 224efbd

11 files changed

Lines changed: 1013 additions & 6 deletions

File tree

packages/b2c-tooling-sdk/src/operations/content/library.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,14 @@ function processContent(
118118
}
119119
}
120120

121-
// Recurse into content-links
121+
// Recurse into content-links (sorted by position)
122122
const contentLinks = content['content-links'] as Array<Record<string, unknown>> | undefined;
123123
if (contentLinks?.[0]?.['content-link']) {
124-
const links = contentLinks[0]['content-link'] as Array<Record<string, unknown>>;
124+
const links = (contentLinks[0]['content-link'] as Array<Record<string, unknown>>).slice().sort((a, b) => {
125+
const posA = parseFloat((a['position'] as string[] | undefined)?.[0] ?? 'Infinity');
126+
const posB = parseFloat((b['position'] as string[] | undefined)?.[0] ?? 'Infinity');
127+
return posA - posB;
128+
});
125129
for (const link of links) {
126130
const linkAttrs = link['$'] as Record<string, string>;
127131
const linkId = linkAttrs['content-id'];

packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ export async function siteArchiveImport(
9898
if (!archiveName) {
9999
throw new Error('archiveName is required when importing from a Buffer');
100100
}
101-
zipFilename = archiveName.endsWith('.zip') ? archiveName : `${archiveName}.zip`;
102-
archiveContent = target;
101+
const baseName = archiveName.endsWith('.zip') ? archiveName.slice(0, -4) : archiveName;
102+
zipFilename = `${baseName}.zip`;
103+
archiveContent = await ensureArchiveStructure(target, baseName, logger);
103104
} else {
104105
// File path - check if directory or zip file
105106
const targetPath = target as string;
@@ -236,6 +237,62 @@ async function addDirectoryToZip(zipFolder: JSZip, dirPath: string): Promise<voi
236237
}
237238
}
238239

240+
/**
241+
* Ensures a zip buffer has the correct top-level directory structure required
242+
* by B2C Commerce site archive import. The archive must contain a single
243+
* top-level directory matching the archive name.
244+
*
245+
* If the zip is already correctly structured, the original buffer is returned.
246+
* Otherwise, the contents are re-wrapped under the expected directory name.
247+
*/
248+
async function ensureArchiveStructure(
249+
buffer: Buffer,
250+
archiveDirName: string,
251+
logger: ReturnType<typeof getLogger>,
252+
): Promise<Buffer> {
253+
let zip: JSZip;
254+
try {
255+
zip = await JSZip.loadAsync(buffer);
256+
} catch {
257+
// If we can't parse the zip, pass it through as-is
258+
logger.debug('Could not parse zip buffer for structure check; passing through as-is');
259+
return buffer;
260+
}
261+
262+
// Determine the unique top-level directory names
263+
const topLevelEntries = new Set<string>();
264+
for (const filePath of Object.keys(zip.files)) {
265+
const topLevel = filePath.split('/')[0];
266+
topLevelEntries.add(topLevel);
267+
}
268+
269+
if (topLevelEntries.size === 1 && topLevelEntries.has(archiveDirName)) {
270+
return buffer; // Already correctly structured
271+
}
272+
273+
// Re-wrap all entries under archiveDirName/
274+
logger.debug(
275+
{archiveDirName, topLevelEntries: [...topLevelEntries]},
276+
`Re-wrapping archive contents under ${archiveDirName}/`,
277+
);
278+
279+
const newZip = new JSZip();
280+
const rootFolder = newZip.folder(archiveDirName)!;
281+
282+
for (const [filePath, entry] of Object.entries(zip.files)) {
283+
if (!entry.dir) {
284+
const content = await entry.async('nodebuffer');
285+
rootFolder.file(filePath, content);
286+
}
287+
}
288+
289+
return newZip.generateAsync({
290+
type: 'nodebuffer',
291+
compression: 'DEFLATE',
292+
compressionOptions: {level: 9},
293+
});
294+
}
295+
239296
/**
240297
* Configuration for sites in export.
241298
*/

packages/b2c-tooling-sdk/test/operations/content/fixtures.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,50 @@ export const WILDCARD_ASSET_LIBRARY_XML = `<?xml version="1.0" encoding="UTF-8"?
102102
</content>
103103
</library>`;
104104

105+
/**
106+
* Library XML with out-of-order content-link positions.
107+
*
108+
* The content-links are listed in XML order: comp-b, comp-d, comp-a, comp-c,
109+
* but their positions specify: comp-a=1, comp-b=2, comp-c=3, comp-d=4.
110+
*/
111+
export const POSITION_LIBRARY_XML = `<?xml version="1.0" encoding="UTF-8"?>
112+
<library xmlns="http://www.demandware.com/xml/impex/library/2006-10-31" library-id="PositionLib">
113+
<content content-id="ordered-page">
114+
<type>page.storePage</type>
115+
<data xml:lang="x-default"><![CDATA[{"title": "Ordered"}]]></data>
116+
<content-links>
117+
<content-link content-id="comp-b" type="page.storePage.main">
118+
<position>2.0</position>
119+
</content-link>
120+
<content-link content-id="comp-d" type="page.storePage.main">
121+
<position>4.0</position>
122+
</content-link>
123+
<content-link content-id="comp-a" type="page.storePage.main">
124+
<position>1.0</position>
125+
</content-link>
126+
<content-link content-id="comp-c" type="page.storePage.main">
127+
<position>3.0</position>
128+
</content-link>
129+
</content-links>
130+
</content>
131+
<content content-id="comp-a">
132+
<type>component.alpha</type>
133+
<data xml:lang="x-default"><![CDATA[{"label": "A"}]]></data>
134+
</content>
135+
<content content-id="comp-b">
136+
<type>component.beta</type>
137+
<data xml:lang="x-default"><![CDATA[{"label": "B"}]]></data>
138+
</content>
139+
<content content-id="comp-c">
140+
<type>component.gamma</type>
141+
<data xml:lang="x-default"><![CDATA[{"label": "C"}]]></data>
142+
</content>
143+
<content content-id="comp-d">
144+
<type>component.delta</type>
145+
<data xml:lang="x-default"><![CDATA[{"label": "D"}]]></data>
146+
</content>
147+
</library>`;
148+
105149
/**
106150
* Library XML with a missing content-link target.
107151
*/

packages/b2c-tooling-sdk/test/operations/content/library.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
MINIMAL_LIBRARY_XML,
1212
WILDCARD_ASSET_LIBRARY_XML,
1313
MISSING_LINK_LIBRARY_XML,
14+
POSITION_LIBRARY_XML,
1415
} from './fixtures.js';
1516

1617
describe('operations/content/library', () => {
@@ -426,6 +427,17 @@ describe('operations/content/library', () => {
426427
});
427428
});
428429

430+
describe('content-link position sorting', () => {
431+
it('should sort children by position rather than XML document order', async () => {
432+
const library = await Library.parse(POSITION_LIBRARY_XML);
433+
const page = library.tree.children.find((n) => n.id === 'ordered-page');
434+
expect(page).to.exist;
435+
436+
const childIds = page!.children.map((n) => n.id);
437+
expect(childIds).to.deep.equal(['comp-a', 'comp-b', 'comp-c', 'comp-d']);
438+
});
439+
});
440+
429441
describe('missing content-link', () => {
430442
it('should parse without error when content-link target is missing', async () => {
431443
const library = await Library.parse(MISSING_LINK_LIBRARY_XML);

packages/b2c-vs-extension/package.json

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
},
2020
"activationEvents": [
2121
"onView:b2cWebdavExplorer",
22+
"onView:b2cContentExplorer",
2223
"onFileSystem:b2c-webdav",
24+
"onFileSystem:b2c-content",
2325
"onCommand:b2c-dx.openUI",
2426
"onCommand:b2c-dx.handleStorefrontNextCartridge",
2527
"onCommand:b2c-dx.promptAgent",
@@ -29,11 +31,17 @@
2931
],
3032
"main": "./dist/extension.js",
3133
"contributes": {
34+
"submenus": [
35+
{
36+
"id": "b2c-dx.submenu",
37+
"label": "B2C-DX"
38+
}
39+
],
3240
"viewsContainers": {
3341
"activitybar": [
3442
{
3543
"id": "b2c-dx",
36-
"title": "B2C-DX WebDAV",
44+
"title": "B2C-DX",
3745
"icon": "media/b2c-icon.svg"
3846
}
3947
]
@@ -42,16 +50,26 @@
4250
"b2c-dx": [
4351
{
4452
"id": "b2cWebdavExplorer",
45-
"name": "Browser",
53+
"name": "WebDAV Browser",
4654
"icon": "media/b2c-icon.svg",
4755
"contextualTitle": "B2C Commerce"
56+
},
57+
{
58+
"id": "b2cContentExplorer",
59+
"name": "Libraries",
60+
"icon": "media/b2c-icon.svg",
61+
"contextualTitle": "B2C Commerce Content"
4862
}
4963
]
5064
},
5165
"viewsWelcome": [
5266
{
5367
"view": "b2cWebdavExplorer",
5468
"contents": "No B2C Commerce instance configured.\n\nCreate a dw.json file in your workspace or set SFCC_* environment variables.\n\n[Refresh](command:b2c-dx.webdav.refresh)"
69+
},
70+
{
71+
"view": "b2cContentExplorer",
72+
"contents": "No content libraries configured.\n\nSet \"contentLibrary\" in dw.json or add a library manually.\n\n[Add Library](command:b2c-dx.content.addLibrary)"
5573
}
5674
],
5775
"commands": [
@@ -132,6 +150,60 @@
132150
"title": "Open as Workspace Folder",
133151
"icon": "$(root-folder-opened)",
134152
"category": "B2C DX"
153+
},
154+
{
155+
"command": "b2c-dx.content.refresh",
156+
"title": "Refresh",
157+
"icon": "$(refresh)",
158+
"category": "B2C DX"
159+
},
160+
{
161+
"command": "b2c-dx.content.addLibrary",
162+
"title": "Add Library",
163+
"icon": "$(add)",
164+
"category": "B2C DX"
165+
},
166+
{
167+
"command": "b2c-dx.content.removeLibrary",
168+
"title": "Remove Library",
169+
"icon": "$(remove)",
170+
"category": "B2C DX"
171+
},
172+
{
173+
"command": "b2c-dx.content.export",
174+
"title": "Export",
175+
"icon": "$(cloud-download)",
176+
"category": "B2C DX"
177+
},
178+
{
179+
"command": "b2c-dx.content.exportNoAssets",
180+
"title": "Export without Assets",
181+
"icon": "$(file-code)",
182+
"category": "B2C DX"
183+
},
184+
{
185+
"command": "b2c-dx.content.exportAssets",
186+
"title": "Export Assets Only",
187+
"icon": "$(file-media)",
188+
"category": "B2C DX"
189+
},
190+
{
191+
"command": "b2c-dx.content.filter",
192+
"title": "Filter",
193+
"icon": "$(filter)",
194+
"category": "B2C DX"
195+
},
196+
{
197+
"command": "b2c-dx.content.clearFilter",
198+
"title": "Clear Filter",
199+
"icon": "$(clear-all)",
200+
"category": "B2C DX"
201+
},
202+
{
203+
"command": "b2c-dx.content.import",
204+
"title": "Import Site Archive",
205+
"icon": "$(cloud-upload)",
206+
"category": "B2C DX"
135207
}
136208
],
137209
"menus": {
@@ -140,6 +212,26 @@
140212
"command": "b2c-dx.webdav.refresh",
141213
"when": "view == b2cWebdavExplorer",
142214
"group": "navigation"
215+
},
216+
{
217+
"command": "b2c-dx.content.refresh",
218+
"when": "view == b2cContentExplorer",
219+
"group": "navigation"
220+
},
221+
{
222+
"command": "b2c-dx.content.addLibrary",
223+
"when": "view == b2cContentExplorer",
224+
"group": "navigation"
225+
},
226+
{
227+
"command": "b2c-dx.content.filter",
228+
"when": "view == b2cContentExplorer && !b2cContentFilterActive",
229+
"group": "navigation"
230+
},
231+
{
232+
"command": "b2c-dx.content.clearFilter",
233+
"when": "view == b2cContentExplorer && b2cContentFilterActive",
234+
"group": "navigation"
143235
}
144236
],
145237
"view/item/context": [
@@ -177,13 +269,44 @@
177269
"command": "b2c-dx.webdav.mountWorkspace",
178270
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|directory)$/",
179271
"group": "3_workspace@1"
272+
},
273+
{
274+
"command": "b2c-dx.content.export",
275+
"when": "view == b2cContentExplorer && viewItem =~ /^(page|content|component)$/",
276+
"group": "1_export@1"
277+
},
278+
{
279+
"command": "b2c-dx.content.exportNoAssets",
280+
"when": "view == b2cContentExplorer && viewItem =~ /^(page|content|component)$/",
281+
"group": "1_export@2"
282+
},
283+
{
284+
"command": "b2c-dx.content.exportAssets",
285+
"when": "view == b2cContentExplorer && viewItem =~ /^(page|content|component)$/",
286+
"group": "1_export@3"
287+
},
288+
{
289+
"command": "b2c-dx.content.removeLibrary",
290+
"when": "view == b2cContentExplorer && viewItem == library",
291+
"group": "2_manage@1"
180292
}
181293
],
182294
"explorer/context": [
183295
{
184296
"command": "b2c-dx.webdav.download",
185297
"when": "resourceScheme == b2c-webdav && !explorerResourceIsFolder",
186298
"group": "navigation"
299+
},
300+
{
301+
"submenu": "b2c-dx.submenu",
302+
"when": "explorerResourceIsFolder",
303+
"group": "7_modification@9"
304+
}
305+
],
306+
"b2c-dx.submenu": [
307+
{
308+
"command": "b2c-dx.content.import",
309+
"when": "explorerResourceIsFolder"
187310
}
188311
]
189312
}

0 commit comments

Comments
 (0)