Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions blocks/browse/da-list/da-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,17 +443,22 @@ export default class DaList extends LitElement {

this._deleteCountLoading = true;
try {
const { crawl } = await import(`${getNx()}/public/utils/tree.js`);
const crawlInstance = crawl({
path: folders.map((folder) => folder.path),
files,
const { source } = await getNx2Api();
const { crawlDeleteCount } = await import('./helpers/utils.js');
// Use the backend-aware source.list (works for both DA and Helix 6) rather
// than nx's crawl, which only lists against the DA origin and returns 0 for
// Helix 6 sites.
const crawlInstance = crawlDeleteCount({
folders,
fileCount: files.length,
source,
concurrent: 5,
});
this._deleteCrawl = crawlInstance;
const allFiles = await crawlInstance.results;
const count = await crawlInstance.results;
// If the user cancelled/closed the dialog while we were crawling, bail out
if (this._confirm !== 'delete' || this._deleteCrawl !== crawlInstance) return;
this._deleteCount = allFiles.length;
this._deleteCount = count;
} finally {
if (this._confirm === 'delete') {
this._deleteCountLoading = false;
Expand Down
54 changes: 54 additions & 0 deletions blocks/browse/da-list/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,60 @@ export async function getFullEntryList(entries) {
return files.filter((file) => file);
}

/**
* Recursively count the files contained within the given folders.
*
* This intentionally uses the backend-aware `source.list` API (which routes to
* the correct origin for both the legacy DA backend and Helix 6) instead of
* nx's `crawl` helper, which lists against the DA origin only and therefore
* returns nothing for Helix 6 sites.
*
* @param {Object} opts
* @param {Array} opts.folders Folder items to crawl (each with a `path`).
* @param {number} [opts.fileCount] Count of already-selected standalone files.
* @param {Object} opts.source The nx2 `source` API (provides `list`).
* @param {number} [opts.concurrent] Max folders listed in parallel.
* @returns {{ results: Promise<number>, cancelCrawl: () => void }}
*/
export function crawlDeleteCount({ folders, fileCount = 0, source, concurrent = 5 }) {
let cancelled = false;

const listFolder = async (path, onCount) => {
const subfolders = [];
let continuationToken;
do {
if (cancelled) break;
const { ok, items, continuationToken: next } = await source.list(
path,
{ continuationToken },
);
if (!ok || !items) break;
items.forEach((item) => {
if (item.ext) onCount();
else subfolders.push(item.path);
});
continuationToken = next;
} while (continuationToken);
return subfolders;
};

const results = (async () => {
let count = fileCount;
const onCount = () => { count += 1; };
const pending = folders.map((folder) => folder.path);

while (pending.length && !cancelled) {
const batch = pending.splice(0, concurrent);
const discovered = await Promise.all(batch.map((path) => listFolder(path, onCount)));
discovered.forEach((subs) => pending.push(...subs));
}

return count;
})();

return { results, cancelCrawl: () => { cancelled = true; } };
}

export function getDropConflicts(list, files) {
const existing = new Set(
list.map((item) => (item.ext ? `${item.name}.${item.ext}` : item.name)),
Expand Down
103 changes: 103 additions & 0 deletions test/unit/blocks/browse/helpers/helpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
handleUpload,
getDropConflicts,
items2Clipboard,
crawlDeleteCount,
} = await import('../../../../../blocks/browse/da-list/helpers/utils.js');

// nx2 api.js pings `/ping/{org}/{site}` to detect hlx6 before every source
Expand Down Expand Up @@ -154,6 +155,108 @@ describe('getDropConflicts', () => {
});
});

describe('crawlDeleteCount', () => {
// Build a fake `source` whose `list` returns the contents of `tree[path]`.
// A folder maps to an array of child items; files have an `ext`, folders don't.
function makeSource(tree) {
const list = async (path) => ({ ok: true, items: tree[path] ?? [], continuationToken: null });
return { list };
}

it('Counts files recursively across nested folders', async () => {
const source = makeSource({
'/org/site/folder': [
{ name: 'a', ext: 'html', path: '/org/site/folder/a.html' },
{ name: 'sub', path: '/org/site/folder/sub' },
],
'/org/site/folder/sub': [
{ name: 'b', ext: 'html', path: '/org/site/folder/sub/b.html' },
{ name: 'c', ext: 'json', path: '/org/site/folder/sub/c.json' },
],
});
const { results } = crawlDeleteCount({
folders: [{ path: '/org/site/folder' }],
source,
});
expect(await results).to.equal(3);
});

it('Includes the already-selected standalone file count', async () => {
const folderItems = [{ name: 'a', ext: 'html', path: '/org/site/folder/a.html' }];
const source = makeSource({ '/org/site/folder': folderItems });
const { results } = crawlDeleteCount({
folders: [{ path: '/org/site/folder' }],
fileCount: 2,
source,
});
expect(await results).to.equal(3);
});

it('Follows pagination via the continuation token', async () => {
let page = 0;
const source = {
list: async (path, { continuationToken } = {}) => {
expect(path).to.equal('/org/site/folder');
if (!continuationToken) {
page += 1;
return {
ok: true,
items: [{ name: 'a', ext: 'html', path: '/org/site/folder/a.html' }],
continuationToken: 'next',
};
}
return {
ok: true,
items: [{ name: 'b', ext: 'html', path: '/org/site/folder/b.html' }],
continuationToken: null,
};
},
};
const { results } = crawlDeleteCount({
folders: [{ path: '/org/site/folder' }],
source,
});
expect(await results).to.equal(2);
expect(page).to.equal(1);
});

it('Stops recursing into subfolders once cancelCrawl is called', async () => {
const listed = [];
const source = {
list: async (path) => {
listed.push(path);
if (path === '/org/site/folder') {
return {
ok: true,
items: [
{ name: 'a', ext: 'html', path: '/org/site/folder/a.html' },
{ name: 'sub', path: '/org/site/folder/sub' },
],
continuationToken: null,
};
}
return { ok: true, items: [], continuationToken: null };
},
};
const crawl = crawlDeleteCount({ folders: [{ path: '/org/site/folder' }], source });
crawl.cancelCrawl();
// The top folder's in-flight listing still resolves, but the discovered
// subfolder is never crawled.
expect(await crawl.results).to.equal(1);
expect(listed).to.deep.equal(['/org/site/folder']);
});

it('Stops recursing when a listing is not ok', async () => {
const source = { list: async () => ({ ok: false, items: [], continuationToken: null }) };
const { results } = crawlDeleteCount({
folders: [{ path: '/org/site/folder' }],
fileCount: 1,
source,
});
expect(await results).to.equal(1);
});
});

describe('items2Clipboard', () => {
let captured;
let savedClipboard;
Expand Down
Loading