Skip to content

Commit b868991

Browse files
authored
feat: WebDAV catalog and library browsing (#316)
* feat: WebDAV catalog and library browsing with configurable mappings Add catalogs and libraries array fields to SDK config so users can specify which catalog/library IDs to browse in the WebDAV tree. The Catalogs and Libraries roots in the WebDAV browser are now virtual roots that show configured IDs as expandable children instead of attempting a PROPFIND on the bare path (which fails). - Add catalogs/libraries to NormalizedConfig, DwJsonConfig, env-source - New WebDavMappingsProvider seeds from config, supports runtime add/remove - Add Catalog / Add Library commands (catalogs use OCAPI discovery) - Browse in WebDAV context menu on library and static asset nodes - contentLibrary falls back to libraries[0] for content tree default * fix: use distinct contextValues for catalog/library virtual roots Prevents Add Catalog from appearing on the Libraries root and vice versa by using virtual-root-catalogs and virtual-root-libraries as contextValues instead of relying on resourcePath matching. * feat: WebDAV drag-and-drop and MOVE/COPY client methods Add copy() and move() methods to the SDK WebDAV client using the standard WebDAV Destination header pattern. Implement TreeDragAndDropController for the WebDAV browser: - Drop files from OS/VS Code explorer to upload (including directories) - Drag files/directories within the tree to move via WebDAV MOVE - Cross-root moves gated behind webdavCrossRootDragDrop setting (off by default) * fix: validate internal MIME payload before treating as tree drop External drops (e.g., from Finder) can produce a truthy but invalid value for the internal tree MIME type. Validate the payload is actually an array of webdav path strings before routing to the tree move handler, otherwise fall through to the external upload handler. * feat: add upload progress notification for drag-and-drop file uploads Show incremental progress with file count and cancellation support when uploading files via drag-and-drop to the WebDAV browser. * fix: remove cross-root drag-and-drop, refresh tree after move - Remove the webdavCrossRootDragDrop setting and cross-root move support (B2C Commerce doesn't support moves between WebDAV roots) - Fire change events after a successful WebDAV MOVE so the source directory updates immediately without requiring a manual refresh
1 parent beaa31e commit b868991

15 files changed

Lines changed: 797 additions & 33 deletions

File tree

packages/b2c-tooling-sdk/src/clients/webdav.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,56 @@ export class WebDavClient {
366366
return await this.parsePropfindResponse(xml);
367367
}
368368

369+
/**
370+
* Copies a file or directory.
371+
*
372+
* @param source - Source path relative to /webdav/Sites/
373+
* @param destination - Destination path relative to /webdav/Sites/
374+
* @param overwrite - Whether to overwrite if destination exists (default: true)
375+
*
376+
* @example
377+
* await client.copy('Cartridges/v1/cartridge', 'Cartridges/v2/cartridge');
378+
*/
379+
async copy(source: string, destination: string, overwrite = true): Promise<void> {
380+
const destUrl = this.buildUrl(destination);
381+
const response = await this.request(source, {
382+
method: 'COPY',
383+
headers: {
384+
Destination: new URL(destUrl).pathname,
385+
Overwrite: overwrite ? 'T' : 'F',
386+
},
387+
});
388+
389+
if (!response.ok) {
390+
throw new HTTPError(`COPY failed: ${response.status} ${response.statusText}`, response, 'COPY');
391+
}
392+
}
393+
394+
/**
395+
* Moves (renames) a file or directory.
396+
*
397+
* @param source - Source path relative to /webdav/Sites/
398+
* @param destination - Destination path relative to /webdav/Sites/
399+
* @param overwrite - Whether to overwrite if destination exists (default: true)
400+
*
401+
* @example
402+
* await client.move('Cartridges/v1/old-name', 'Cartridges/v1/new-name');
403+
*/
404+
async move(source: string, destination: string, overwrite = true): Promise<void> {
405+
const destUrl = this.buildUrl(destination);
406+
const response = await this.request(source, {
407+
method: 'MOVE',
408+
headers: {
409+
Destination: new URL(destUrl).pathname,
410+
Overwrite: overwrite ? 'T' : 'F',
411+
},
412+
});
413+
414+
if (!response.ok) {
415+
throw new HTTPError(`MOVE failed: ${response.status} ${response.statusText}`, response, 'MOVE');
416+
}
417+
}
418+
369419
/**
370420
* Checks if a path exists.
371421
*

packages/b2c-tooling-sdk/src/config/dw-json.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export interface DwJsonConfig {
7979
cartridges?: string | string[];
8080
/** Default content library ID for content export/list commands */
8181
contentLibrary?: string;
82+
/** Catalog IDs for WebDAV browsing */
83+
catalogs?: string[];
84+
/** Library IDs for WebDAV browsing */
85+
libraries?: string[];
8286
/** Optional CIP analytics host override */
8387
cipHost?: string;
8488
/** Path to PKCS12 certificate file for mTLS (two-factor auth) */

packages/b2c-tooling-sdk/src/config/mapping.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ export function mapDwJsonToNormalizedConfig(json: DwJsonConfig): NormalizedConfi
155155
realm: json.realm,
156156
cartridges: parseCartridges(json.cartridges),
157157
contentLibrary: json.contentLibrary,
158+
catalogs: json.catalogs,
159+
libraries: json.libraries,
158160
cipHost: json.cipHost,
159161
instanceName: json.name,
160162
authMethods: json.authMethods,
@@ -241,6 +243,12 @@ export function mapNormalizedConfigToDwJson(config: Partial<NormalizedConfig>, n
241243
if (config.cartridges !== undefined) {
242244
result.cartridges = config.cartridges;
243245
}
246+
if (config.catalogs !== undefined) {
247+
result.catalogs = config.catalogs;
248+
}
249+
if (config.libraries !== undefined) {
250+
result.libraries = config.libraries;
251+
}
244252
if (config.cipHost !== undefined) {
245253
result.cipHost = config.cipHost;
246254
}
@@ -367,6 +375,8 @@ export function mergeConfigsWithProtection(
367375
tenantId: overrides.tenantId ?? base.tenantId,
368376
cartridges: overrides.cartridges ?? base.cartridges,
369377
contentLibrary: overrides.contentLibrary ?? base.contentLibrary,
378+
catalogs: overrides.catalogs ?? base.catalogs,
379+
libraries: overrides.libraries ?? base.libraries,
370380
cipHost: overrides.cipHost ?? base.cipHost,
371381
sandboxApiHost: overrides.sandboxApiHost ?? base.sandboxApiHost,
372382
realm: overrides.realm ?? base.realm,

packages/b2c-tooling-sdk/src/config/sources/env-source.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const ENV_VAR_MAP: Record<string, keyof NormalizedConfig> = {
3838
SFCC_SHORTCODE: 'shortCode',
3939
SFCC_TENANT_ID: 'tenantId',
4040
SFCC_CARTRIDGES: 'cartridges',
41+
SFCC_CATALOGS: 'catalogs',
42+
SFCC_LIBRARIES: 'libraries',
4143
SFCC_AUTH_METHODS: 'authMethods',
4244
SFCC_ACCOUNT_MANAGER_HOST: 'accountManagerHost',
4345
SFCC_SANDBOX_API_HOST: 'sandboxApiHost',
@@ -53,7 +55,7 @@ const ENV_VAR_MAP: Record<string, keyof NormalizedConfig> = {
5355
};
5456

5557
/** Fields that should be parsed as comma-separated arrays. */
56-
const ARRAY_FIELDS = new Set<keyof NormalizedConfig>(['scopes', 'authMethods', 'cartridges']);
58+
const ARRAY_FIELDS = new Set<keyof NormalizedConfig>(['scopes', 'authMethods', 'cartridges', 'catalogs', 'libraries']);
5759

5860
/** Fields that should be parsed as booleans. */
5961
const BOOLEAN_FIELDS = new Set<keyof NormalizedConfig>(['selfSigned']);

packages/b2c-tooling-sdk/src/config/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ export interface NormalizedConfig {
9595
/** Default content library ID for content export/list commands */
9696
contentLibrary?: string;
9797

98+
/** Catalog IDs for WebDAV browsing */
99+
catalogs?: string[];
100+
101+
/** Library IDs for WebDAV browsing */
102+
libraries?: string[];
103+
98104
// CIP
99105
/** Optional CIP analytics host override */
100106
cipHost?: string;

packages/b2c-vs-extension/package.json

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,36 @@
316316
"icon": "$(root-folder-opened)",
317317
"category": "B2C DX"
318318
},
319+
{
320+
"command": "b2c-dx.webdav.addCatalog",
321+
"title": "Add Catalog",
322+
"icon": "$(add)",
323+
"category": "B2C DX"
324+
},
325+
{
326+
"command": "b2c-dx.webdav.removeCatalog",
327+
"title": "Remove Catalog",
328+
"icon": "$(remove)",
329+
"category": "B2C DX"
330+
},
331+
{
332+
"command": "b2c-dx.webdav.addLibrary",
333+
"title": "Add Library",
334+
"icon": "$(add)",
335+
"category": "B2C DX"
336+
},
337+
{
338+
"command": "b2c-dx.webdav.removeLibrary",
339+
"title": "Remove Library",
340+
"icon": "$(remove)",
341+
"category": "B2C DX"
342+
},
343+
{
344+
"command": "b2c-dx.content.browseWebdav",
345+
"title": "Browse in WebDAV",
346+
"icon": "$(folder-opened)",
347+
"category": "B2C DX"
348+
},
319349
{
320350
"command": "b2c-dx.content.refresh",
321351
"title": "Refresh",
@@ -431,19 +461,39 @@
431461
}
432462
],
433463
"view/item/context": [
464+
{
465+
"command": "b2c-dx.webdav.addCatalog",
466+
"when": "view == b2cWebdavExplorer && viewItem == virtual-root-catalogs",
467+
"group": "0_add@0"
468+
},
469+
{
470+
"command": "b2c-dx.webdav.addLibrary",
471+
"when": "view == b2cWebdavExplorer && viewItem == virtual-root-libraries",
472+
"group": "0_add@0"
473+
},
474+
{
475+
"command": "b2c-dx.webdav.removeCatalog",
476+
"when": "view == b2cWebdavExplorer && viewItem == catalog-mapping",
477+
"group": "2_manage@1"
478+
},
479+
{
480+
"command": "b2c-dx.webdav.removeLibrary",
481+
"when": "view == b2cWebdavExplorer && viewItem == library-mapping",
482+
"group": "2_manage@1"
483+
},
434484
{
435485
"command": "b2c-dx.webdav.newFile",
436-
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|directory)$/",
486+
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|catalog-mapping|library-mapping|directory)$/",
437487
"group": "1_modification@0"
438488
},
439489
{
440490
"command": "b2c-dx.webdav.newFolder",
441-
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|directory)$/",
491+
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|catalog-mapping|library-mapping|directory)$/",
442492
"group": "1_modification@1"
443493
},
444494
{
445495
"command": "b2c-dx.webdav.uploadFile",
446-
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|directory)$/",
496+
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|catalog-mapping|library-mapping|directory)$/",
447497
"group": "1_modification@2"
448498
},
449499
{
@@ -463,7 +513,7 @@
463513
},
464514
{
465515
"command": "b2c-dx.webdav.mountWorkspace",
466-
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|directory)$/",
516+
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|catalog-mapping|library-mapping|directory)$/",
467517
"group": "3_workspace@1"
468518
},
469519
{
@@ -486,6 +536,11 @@
486536
"when": "view == b2cContentExplorer && viewItem == library",
487537
"group": "2_manage@1"
488538
},
539+
{
540+
"command": "b2c-dx.content.browseWebdav",
541+
"when": "view == b2cContentExplorer && viewItem =~ /^(library|static)$/",
542+
"group": "3_webdav@1"
543+
},
489544
{
490545
"command": "b2c-dx.sandbox.create",
491546
"when": "view == b2cSandboxExplorer && viewItem == realm",
@@ -605,6 +660,18 @@
605660
"command": "b2c-dx.sandbox.extendExpiration",
606661
"when": "false"
607662
},
663+
{
664+
"command": "b2c-dx.webdav.removeCatalog",
665+
"when": "false"
666+
},
667+
{
668+
"command": "b2c-dx.webdav.removeLibrary",
669+
"when": "false"
670+
},
671+
{
672+
"command": "b2c-dx.content.browseWebdav",
673+
"when": "false"
674+
},
608675
{
609676
"command": "b2c-dx.webdav.newFolder",
610677
"when": "false"

packages/b2c-vs-extension/src/content-tree/content-commands.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,29 @@ export function registerContentCommands(
236236
vscode.window.showInformationMessage('Site archive imported successfully.');
237237
});
238238

239-
return [refresh, addLibrary, removeLibrary, exportCmd, exportNoAssets, exportAssets, filter, clearFilter, importCmd];
239+
const browseWebdav = vscode.commands.registerCommand('b2c-dx.content.browseWebdav', async (node: ContentTreeItem) => {
240+
if (!node) return;
241+
242+
if (node.nodeType === 'library') {
243+
await vscode.commands.executeCommand('b2c-dx.webdav.revealLibrary', node.libraryId);
244+
} else if (node.nodeType === 'static') {
245+
// Static assets have a webdav path: Libraries/{libraryId}/default/{path}
246+
const cleanPath = node.contentId.startsWith('/') ? node.contentId.slice(1) : node.contentId;
247+
const webdavPath = `Libraries/${node.libraryId}/default/${cleanPath}`;
248+
await vscode.commands.executeCommand('b2c-dx.webdav.revealPath', webdavPath);
249+
}
250+
});
251+
252+
return [
253+
refresh,
254+
addLibrary,
255+
removeLibrary,
256+
exportCmd,
257+
exportNoAssets,
258+
exportAssets,
259+
filter,
260+
clearFilter,
261+
importCmd,
262+
browseWebdav,
263+
];
240264
}

packages/b2c-vs-extension/src/content-tree/content-config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export class ContentConfigProvider {
3232
}
3333

3434
getContentLibrary(): string | undefined {
35-
return this.configProvider.getConfig()?.values.contentLibrary;
35+
const config = this.configProvider.getConfig();
36+
return config?.values.contentLibrary ?? config?.values.libraries?.[0];
3637
}
3738

3839
getLibraries(): BrowsedLibrary[] {

packages/b2c-vs-extension/src/content-tree/content-tree-provider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ export class ContentTreeDataProvider implements vscode.TreeDataProvider<ContentT
142142
return [];
143143
}
144144

145-
// Auto-add configured library if list is empty
145+
// Auto-add configured library if list is empty.
146+
// Prefer explicit contentLibrary, fall back to libraries[0] from config.
146147
const libraries = this.configProvider.getLibraries();
147148
if (libraries.length === 0) {
148149
const contentLibrary = this.configProvider.getContentLibrary();

packages/b2c-vs-extension/src/webdav-tree/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
*/
66
import * as vscode from 'vscode';
77
import type {B2CExtensionConfig} from '../config-provider.js';
8+
import {WebDavDragAndDropController} from './webdav-dnd-controller.js';
89
import {WEBDAV_SCHEME, WebDavFileSystemProvider} from './webdav-fs-provider.js';
10+
import {WebDavMappingsProvider} from './webdav-mappings.js';
911
import {WebDavTreeDataProvider} from './webdav-tree-provider.js';
1012
import {registerWebDavCommands} from './webdav-commands.js';
1113

@@ -16,14 +18,27 @@ export function registerWebDavTree(context: vscode.ExtensionContext, configProvi
1618
isCaseSensitive: true,
1719
});
1820

19-
const treeProvider = new WebDavTreeDataProvider(configProvider, fsProvider);
21+
const mappingsProvider = new WebDavMappingsProvider(configProvider);
22+
mappingsProvider.seedFromConfig();
23+
24+
const treeProvider = new WebDavTreeDataProvider(configProvider, fsProvider, mappingsProvider);
25+
26+
const dndController = new WebDavDragAndDropController(configProvider, fsProvider);
2027

2128
const treeView = vscode.window.createTreeView('b2cWebdavExplorer', {
2229
treeDataProvider: treeProvider,
2330
showCollapseAll: true,
31+
dragAndDropController: dndController,
2432
});
2533

26-
const commandDisposables = registerWebDavCommands(context, configProvider, treeProvider, fsProvider);
34+
const commandDisposables = registerWebDavCommands(
35+
context,
36+
configProvider,
37+
treeProvider,
38+
treeView,
39+
fsProvider,
40+
mappingsProvider,
41+
);
2742

2843
// Auto-refresh when config changes (dw.json edit, manual reset, future instance switch)
2944
configProvider.onDidReset(() => {

0 commit comments

Comments
 (0)