import { state, property, customElement } from "lit/decorators.js";
import { msg, localized, str } from "@lit/localize";
import { when } from "lit/directives/when.js";
import debounce from "lodash/fp/debounce";
import type { SlMenuItem, SlIconButton } from "@shoelace-style/shoelace";
import queryString from "query-string";
import type { AuthState } from "../utils/AuthService";
import type { Collection, CollectionList } from "../types/collection";
import LiteElement, { html } from "../utils/LiteElement";
import type {
APIPaginatedList,
APIPaginationQuery,
APISortQuery,
} from "../types/api";
const INITIAL_PAGE_SIZE = 10;
const MIN_SEARCH_LENGTH = 2;
type CollectionSearchResults = APIPaginatedList & {
items: CollectionList;
};
export type CollectionsChangeEvent = CustomEvent<{
collections: string[];
}>;
/**
* Usage:
* ```ts
*
* ```
* @events collections-change
*/
@localized()
@customElement("btrix-collections-add")
export class CollectionsAdd extends LiteElement {
@property({ type: Object })
authState!: AuthState;
@property({ type: Array })
initialCollections?: string[];
@property({ type: String })
orgId!: string;
@property({ type: String })
configId!: string;
@property({ type: String })
label?: string;
/* Text to show on collection empty state */
@property({ type: String })
emptyText?: string;
@state()
private collectionsData: { [id: string]: Collection } = {};
@state()
private collectionIds: string[] = [];
@state()
private searchByValue: string = "";
@state()
private searchResults: CollectionList = [];
private get hasSearchStr() {
return this.searchByValue.length >= MIN_SEARCH_LENGTH;
}
@state()
private searchResultsOpen = false;
connectedCallback() {
if (this.initialCollections) {
this.collectionIds = this.initialCollections;
}
super.connectedCallback();
this.initializeCollectionsFromIds();
}
disconnectedCallback() {
this.onSearchInput.cancel();
super.disconnectedCallback();
}
render() {
return html`
${this.renderSearch()}
${when(this.collectionIds, () =>
this.collectionIds.length
? html`
${this.collectionIds.map(this.renderCollectionItem, this)}
`
: this.emptyText
? html`
`
: ""
)}
`;
}
private renderSearch() {
return html`
{
this.searchResultsOpen = false;
this.searchByValue = "";
}}
@sl-select=${async (e: CustomEvent) => {
this.searchResultsOpen = false;
const item = e.detail.item as SlMenuItem;
const collId = item.dataset["key"];
if (collId && this.collectionIds.indexOf(collId) === -1) {
const coll = this.searchResults.find(
(collection) => collection.id === collId
);
if (coll) {
const { id } = coll;
if (!this.collectionsData[id]) {
this.collectionsData = {
...this.collectionsData,
[id]: await this.getCollection(id),
};
}
this.collectionIds = [...this.collectionIds, id];
this.dispatchChange();
}
}
}}
>
{
this.searchResultsOpen = false;
this.onSearchInput.cancel();
}}
@sl-input=${this.onSearchInput}
>
${this.renderSearchResults()}
`;
}
private renderSearchResults() {
if (!this.hasSearchStr) {
return html`
${msg("Start typing to search Collections.")}
`;
}
// Filter out stale search results from last debounce invocation
const searchResults = this.searchResults.filter((res) =>
new RegExp(`^${this.searchByValue}`, "i").test(res.name)
);
if (!searchResults.length) {
return html`
${msg("No matching Collections found.")}
`;
}
return html`
${searchResults.map((item: Collection) => {
return html`
${item.name}
${msg(str`${item.crawlCount} items`)}
`;
})}
`;
}
private renderCollectionItem(id: string) {
const collection = this.collectionsData[id];
return html`
${collection?.name}
${msg(str`${collection?.crawlCount || 0} items`)}
`;
}
private removeCollection(event: Event) {
const target = event.currentTarget as HTMLElement;
const collectionId = target.getAttribute("data-key");
if (collectionId) {
const collIdIndex = this.collectionIds.indexOf(collectionId);
if (collIdIndex > -1) {
this.collectionIds = [
...this.collectionIds.slice(0, collIdIndex),
...this.collectionIds.slice(collIdIndex + 1),
];
this.dispatchChange();
}
}
}
private onSearchInput = debounce(200)(async (e: any) => {
this.searchByValue = e.target.value.trim();
if (this.searchResultsOpen === false && this.hasSearchStr) {
this.searchResultsOpen = true;
}
const data: CollectionSearchResults | undefined =
await this.fetchCollectionsByPrefix(this.searchByValue);
let searchResults: CollectionList = [];
if (data && data.items.length) {
searchResults = this.filterOutSelectedCollections(data.items);
}
this.searchResults = searchResults;
}) as any;
private filterOutSelectedCollections(results: CollectionList) {
return results.filter((result) => {
return !this.collectionIds.some((id) => id === result.id);
});
}
private async fetchCollectionsByPrefix(namePrefix: string) {
try {
const results: CollectionSearchResults = await this.getCollections({
oid: this.orgId,
namePrefix: namePrefix,
sortBy: "name",
pageSize: INITIAL_PAGE_SIZE,
});
return results;
} catch {
this.notify({
message: msg("Sorry, couldn't retrieve Collections at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async getCollections(
params: Partial<{
oid?: string;
namePrefix?: string;
}> &
APIPaginationQuery &
APISortQuery
): Promise {
const query = queryString.stringify(params || {}, {
arrayFormat: "comma",
});
const data: APIPaginatedList = await this.apiFetch(
`/orgs/${this.orgId}/collections?${query}`,
this.authState!
);
return data;
}
private async initializeCollectionsFromIds() {
if (!this.collectionIds) return;
this.collectionIds.forEach(async (id) => {
const data = await this.getCollection(id);
if (data) {
this.collectionsData = {
...this.collectionsData,
[id]: data,
};
}
});
}
private getCollection = (collId: string): Promise => {
return this.apiFetch(
`/orgs/${this.orgId}/collections/${collId}`,
this.authState!
);
};
private async dispatchChange() {
await this.updateComplete;
this.dispatchEvent(
new CustomEvent("collections-change", {
detail: { collections: this.collectionIds },
})
);
}
}