Skip to content
41 changes: 30 additions & 11 deletions blocks/browse/da-browse/da-browse.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ export default class DaBrowse extends LitElement {
return this.shadowRoot.querySelector('da-new');
}

get browseListItems() {
// eslint-disable-next-line no-underscore-dangle
return this.shadowRoot.querySelector('.da-list-type-browse')?._listItems || [];
}

isRootFolder(path) {
return path.split('/').length <= 2;
}

renderNew() {
return html`
<da-new
Expand All @@ -139,7 +148,12 @@ export default class DaBrowse extends LitElement {
}

renderSearch() {
return html`<da-search @updated=${this.handleSearch} fullpath="${this.details.fullpath}"></da-search>`;
return html`
<da-search
@updated=${this.handleSearch}
fullpath="${this.details.fullpath}"
.browseItems="${this.browseListItems}">
</da-search>`;
}

renderList(type, fullpath, select, sort, drag) {
Expand All @@ -157,16 +171,21 @@ export default class DaBrowse extends LitElement {
render() {
return html`
<div class="da-tablist" role="tablist" aria-label="Dark Alley content">
${this._tabItems.map((tab, idx) => html`
<button
id="tab-${tab.id}"
type="button"
role="tab"
aria-selected="${tab.selected}"
aria-controls="tabpanel-${tab.id}"
@click=${() => { this.handleTabClick(idx); }}>
<span class="focus">${tab.title}</span>
</button>`)}
${this._tabItems.map((tab, idx) => {
if (tab.id === 'search' && this.isRootFolder(this.details.fullpath)) {
return nothing;
}
return html`
<button
id="tab-${tab.id}"
type="button"
role="tab"
aria-selected="${tab.selected}"
aria-controls="tabpanel-${tab.id}"
@click=${() => { this.handleTabClick(idx); }}>
<span class="focus">${tab.title}</span>
</button>`;
})}
</div>
<div class="da-list-header context-${this.context}">
<da-breadcrumbs fullpath="${this.details.fullpath}" depth="${this.details.depth}"></da-breadcrumbs>
Expand Down
55 changes: 54 additions & 1 deletion blocks/browse/da-search/da-search.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,27 @@ const { crawl, Queue } = await import(`${getNx()}/public/utils/tree.js`);
const { default: getStyle } = await import(`${getNx()}/utils/styles.js`);
const STYLE = await getStyle(import.meta.url);

const DEFAULT_LOCALES = ['langstore'];

function getLocales(translate) {
const locales = new Set(DEFAULT_LOCALES);

translate?.languages?.data?.forEach((lang) => {
lang.locales?.split(',').forEach((loc) => {
const dir = loc.split('/').find((part) => part?.trim() !== '');
if (dir) {
locales.add(dir.trim());
}
});
});

return locales;
}

export default class DaSearch extends LitElement {
static properties = {
fullpath: { type: String },
browseItems: { type: Array },
_term: { state: true },
_total: { state: true },
_matches: { state: true },
Expand Down Expand Up @@ -56,6 +74,40 @@ export default class DaSearch extends LitElement {
this._time = null;
}

async getSearchScope(startPath) {
const isSiteFolder = startPath.split('/').length === 3;
if (!isSiteFolder) {
return { paths: [startPath], files: [] };
}

const resp = await daFetch(`${DA_ORIGIN}/source${startPath}/.da/translate.json`);
if (!resp.ok) {
return { paths: [startPath], files: [] };
}

const translate = await resp.json();
const locales = getLocales(translate);

if (!locales.size || !this.browseItems?.length) {
return { paths: [startPath], files: [] };
}

const paths = [];
const files = [];

this.browseItems.forEach((item) => {
if (!locales.has(item.name)) {
if (item.ext) {
files.push(item);
} else {
paths.push(item.path);
}
}
});

return { paths, files };
}

async getMatches(startPath, term) {
const searchTypes = ['.html', '.json', '.svg'];

Expand Down Expand Up @@ -106,7 +158,8 @@ export default class DaSearch extends LitElement {
}
};

const { results } = crawl({ path: startPath, callback: searchFile, throttle: 10 });
const { paths, files } = await this.getSearchScope(startPath);
const { results } = crawl({ path: paths, callback: searchFile, throttle: 10, files });
await results;
}

Expand Down
116 changes: 116 additions & 0 deletions test/unit/blocks/browse/da-browse/da-browse.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
eslint-disable no-underscore-dangle
*/
import { expect } from '@esm-bundle/chai';

// This is needed to make a dynamic import work that is indirectly referenced
// from da-browse.js
const { setNx } = await import('../../../../../scripts/utils.js');
setNx('/bheuaark/', { hostname: 'localhost' });

const { default: DaBrowse } = await import('../../../../../blocks/browse/da-list/da-list.js');
const { default: DaBrowseComponent } = await import('../../../../../blocks/browse/da-browse/da-browse.js');

describe('Browse', () => {
it('Pasted item uses the target URL', async () => {
const daBrowse = new DaBrowse();

const fetchedArgs = [];
const mockFetch = async (url, opts) => {
fetchedArgs.push({ url, opts });
return {
ok: true,
json: async () => ({}),
headers: {
get: () => {

},
},
};
};

const item = {
path: '/myorg/mysite/myroot/srcdir/d1.html',
ext: 'html',
isChecked: true,
name: 'd1',
};
daBrowse._listItems = [];
daBrowse._selectedItems = [item];
daBrowse.fullpath = '/myorg/mysite/myroot/destdir';

const orgFetch = window.fetch;
try {
window.fetch = mockFetch;
await daBrowse.handlePaste({});

expect(daBrowse._listItems.length).to.equal(1);
expect(daBrowse._listItems[0].path).to.equal('/myorg/mysite/myroot/destdir/d1.html');
expect(daBrowse._listItems[0].ext).to.equal('html');
expect(daBrowse._listItems[0].isChecked).to.be.false;
expect(daBrowse._listItems[0].name).to.equal('d1');

expect(fetchedArgs.length).to.equal(1);
expect(fetchedArgs[0].url).to.equal('https://admin.da.live/copy/myorg/mysite/myroot/srcdir/d1.html');
expect(fetchedArgs[0].opts.body.get('destination')).to.equal('/myorg/mysite/myroot/destdir/d1.html');
expect(fetchedArgs[0].opts.method).to.equal('POST');
} finally {
window.fetch = orgFetch;
}
});
});

describe('DaBrowse Component', () => {
let daBrowseComp;

beforeEach(() => {
daBrowseComp = new DaBrowseComponent();
daBrowseComp.details = { fullpath: '/myorg/mysite/folder', owner: 'myorg', depth: 3 };
});

describe('isRootFolder', () => {
it('returns true for root path (org only)', () => {
expect(daBrowseComp.isRootFolder('/myorg')).to.be.true;
});

it('returns false for org/site path (length = 3)', () => {
// '/myorg/mysite' splits into ['', 'myorg', 'mysite'] which has length 3
expect(daBrowseComp.isRootFolder('/myorg/mysite')).to.be.false;
});

it('returns false for paths deeper than org/site', () => {
expect(daBrowseComp.isRootFolder('/myorg/mysite/folder')).to.be.false;
expect(daBrowseComp.isRootFolder('/myorg/mysite/folder/subfolder')).to.be.false;
});

it('returns true for empty path', () => {
expect(daBrowseComp.isRootFolder('')).to.be.true;
});

it('returns true for single slash', () => {
expect(daBrowseComp.isRootFolder('/')).to.be.true;
});
});

describe('browseListItems getter', () => {
beforeEach(async () => {
// Properly initialize the component by adding to DOM
document.body.innerHTML = '<div id="container"></div>';
const container = document.getElementById('container');
container.appendChild(daBrowseComp);
await daBrowseComp.updateComplete;
});

afterEach(() => {
document.body.innerHTML = '';
});

it('returns empty array when browse list is not present', () => {
expect(daBrowseComp.browseListItems).to.deep.equal([]);
});

it('returns empty array when browse list has no _listItems', () => {
expect(daBrowseComp.browseListItems).to.deep.equal([]);
});
});
});
Loading
Loading