Manage collection from archived item details (#1085)

- Lists collections that an archived item belongs to in item detail view
- Improves performance of collection add component
---------

Co-authored-by: Tessa Walsh <tessa@bitarchivist.net>
This commit is contained in:
sua yoo 2023-09-05 14:52:17 -07:00 committed by GitHub
parent 00eddd548d
commit ff6650d481
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 140 additions and 60 deletions

View File

@ -60,7 +60,7 @@ export class CollectionsAdd extends LiteElement {
emptyText?: string;
@state()
private collections: CollectionList = [];
private collectionsData: { [id: string]: Collection } = {};
@state()
private collectionIds: string[] = [];
@ -78,12 +78,17 @@ export class CollectionsAdd extends LiteElement {
@state()
private searchResultsOpen = false;
async connectedCallback() {
connectedCallback() {
if (this.initialCollections) {
this.collectionIds = this.initialCollections;
}
await this.initializeCollectionsFromIds();
super.connectedCallback();
this.initializeCollectionsFromIds();
}
disconnectedCallback() {
this.onSearchInput.cancel();
super.disconnectedCallback();
}
render() {
@ -95,12 +100,12 @@ export class CollectionsAdd extends LiteElement {
${this.renderSearch()}
</div>
${when(this.collections, () =>
this.collections.length
${when(this.collectionIds, () =>
this.collectionIds.length
? html`
<div class="mb-2">
<ul class="contents">
${this.collections.map(this.renderCollectionItem, this)}
${this.collectionIds.map(this.renderCollectionItem, this)}
</ul>
</div>
`
@ -132,12 +137,17 @@ export class CollectionsAdd extends LiteElement {
(collection) => collection.id === collId
);
if (coll) {
this.collections.push(coll);
this.collectionIds.push(coll.id);
await this.dispatchChange();
const { id } = coll;
if (!this.collectionsData[id]) {
this.collectionsData = {
...this.collectionsData,
[id]: await this.getCollection(id),
};
}
this.collectionIds = [...this.collectionIds, id];
this.dispatchChange();
}
}
await this.updateComplete;
}}
>
<sl-input
@ -167,7 +177,12 @@ export class CollectionsAdd extends LiteElement {
`;
}
if (!this.searchResults.length) {
// 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`
<sl-menu-item slot="menu-item" disabled
>${msg("No matching Collections found.")}</sl-menu-item
@ -176,7 +191,7 @@ export class CollectionsAdd extends LiteElement {
}
return html`
${this.searchResults.map((item: Collection) => {
${searchResults.map((item: Collection) => {
return html`
<sl-menu-item class="w-full" slot="menu-item" data-key=${item.id}>
<div class="flex w-full gap-2 items-center">
@ -193,40 +208,44 @@ export class CollectionsAdd extends LiteElement {
`;
}
private renderCollectionItem(collection: Collection) {
return html`<li class="mt-1 p-2 pl-5 pr-5 border rounded-sm">
<div class="flex flex-row gap-2 justify-between items-center">
<div class="justify-self-stretch grow truncate">${
collection.name
}</div>
<div class="text-neutral-500 text-xs text-right font-monostyle">
${msg(str`${collection.crawlCount} Crawls`)}
</div>
<sl-icon-button
name="x-lg"
data-key=${collection.id}
@click=${this.removeCollection}>
</sl-icon-button>
</dib>
</li>`;
private renderCollectionItem(id: string) {
const collection = this.collectionsData[id];
return html`<li class="mt-1 p-1 pl-3 border rounded-sm">
<div
class="flex flex-row gap-2 justify-between items-center transition-opacity delay-75 ${collection
? "opacity-100"
: "opacity-0"}"
>
<div class="justify-self-stretch grow truncate">
${collection?.name}
</div>
<div class="text-neutral-500 text-xs text-right font-monostyle">
${msg(str`${collection?.crawlCount || 0} Crawls`)}
</div>
<sl-icon-button
name="x-lg"
data-key=${id}
?disabled=${!collection}
@click=${this.removeCollection}
>
</sl-icon-button>
</div>
</li>`;
}
private async removeCollection(event: Event) {
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.splice(collIdIndex, 1);
}
const collIndex = this.collections.findIndex(
(collection) => collection.id === collectionId
);
if (collIndex > -1) {
this.collections.splice(collIndex, 1);
this.collectionIds = [
...this.collectionIds.slice(0, collIdIndex),
...this.collectionIds.slice(collIdIndex + 1),
];
this.dispatchChange();
}
}
await this.requestUpdate();
}
private onSearchInput = debounce(200)(async (e: any) => {
@ -247,9 +266,7 @@ export class CollectionsAdd extends LiteElement {
private filterOutSelectedCollections(results: CollectionList) {
return results.filter((result) => {
return this.collections.every((coll) => {
return coll.id !== result.id;
});
return !this.collectionIds.some((id) => id === result.id);
});
}
@ -291,18 +308,25 @@ export class CollectionsAdd extends LiteElement {
}
private async initializeCollectionsFromIds() {
for (let i = 0; i < this.collectionIds?.length; i++) {
const collId = this.collectionIds[i];
const data: Collection = await this.apiFetch(
`/orgs/${this.orgId}/collections/${collId}`,
this.authState!
);
if (!this.collectionIds) return;
this.collectionIds.forEach(async (id) => {
const data = await this.getCollection(id);
if (data) {
this.collections.push(data);
this.collectionsData = {
...this.collectionsData,
[id]: data,
};
}
}
});
}
private getCollection = (collId: string): Promise<Collection> => {
return this.apiFetch(
`/orgs/${this.orgId}/collections/${collId}`,
this.authState!
);
};
private async dispatchChange() {
await this.updateComplete;
this.dispatchEvent(

View File

@ -53,6 +53,9 @@ export class CrawlMetadataEditor extends LiteElement {
@state()
private tagsToSave: Tags = [];
@state()
private collectionsToSave: string[] = [];
// For fuzzy search:
private fuse = new Fuse([], {
shouldSort: false,
@ -68,6 +71,7 @@ export class CrawlMetadataEditor extends LiteElement {
if (changedProperties.has("crawl") && this.crawl) {
this.includeName = this.crawl.type === "upload";
this.tagsToSave = this.crawl.tags || [];
this.collectionsToSave = this.crawl.collectionIds || [];
}
}
@ -121,6 +125,18 @@ export class CrawlMetadataEditor extends LiteElement {
@tags-change=${(e: TagsChangeEvent) =>
(this.tagsToSave = e.detail.tags)}
></btrix-tag-input>
<div class="mt-4">
<btrix-collections-add
.authState=${this.authState}
.initialCollections=${this.crawl.collectionIds}
.orgId=${this.crawl.oid}
.configId=${"temp"}
label=${msg("Add to Collection")}
@collections-change=${(e: CustomEvent) =>
(this.collectionsToSave = e.detail.collections)}
>
</btrix-collections-add>
</div>
</form>
<div slot="footer" class="flex justify-between">
<sl-button form="crawlDetailsForm" type="reset" size="small"
@ -173,29 +189,42 @@ export class CrawlMetadataEditor extends LiteElement {
if (!(await this.checkFormValidity(formEl))) return;
const { crawlDescription, name } = serialize(formEl);
const params: {
collectionIds?: string[];
tags?: string[];
description?: string;
name?: string;
} = {};
if (this.includeName && name && name !== this.crawl.name) {
params.name = name as string;
}
if (
(!this.includeName || name === this.crawl.name) &&
crawlDescription === (this.crawl!.description ?? "") &&
JSON.stringify(this.tagsToSave) === JSON.stringify(this.crawl!.tags)
crawlDescription &&
crawlDescription !== (this.crawl.description ?? "")
) {
params.description = crawlDescription as string;
}
if (JSON.stringify(this.tagsToSave) !== JSON.stringify(this.crawl.tags)) {
params.tags = this.tagsToSave;
}
if (
JSON.stringify(this.collectionsToSave) !==
JSON.stringify(this.crawl.collectionIds)
) {
params.collectionIds = this.collectionsToSave;
}
if (!Object.keys(params).length) {
// No changes have been made
this.requestClose();
return;
}
const params = {
tags: this.tagsToSave,
description: crawlDescription,
name,
};
this.isSubmittingUpdate = true;
try {
const data = await this.apiFetch(
`/orgs/${this.crawl!.oid}/${
this.crawl!.type === "crawl" ? "crawls" : "uploads"
}/${this.crawl.id}`,
`/orgs/${this.crawl!.oid}/all-crawls/${this.crawl.id}`,
this.authState!,
{
method: "PATCH",

View File

@ -750,6 +750,32 @@ ${this.crawl?.description}
() => html`<sl-skeleton></sl-skeleton>`
)}
</btrix-desc-list-item>
<btrix-desc-list-item label=${msg("In Collections")}>
${when(
this.crawl,
() =>
when(
this.crawl!.collections.length,
() => html`
<ul>
${this.crawl!.collections.map(
({ id, name }) =>
html`<li class="mt-1">
<a
class="text-primary hover:text-indigo-400"
href=${`/orgs/${this.orgId}/collections/view/${id}`}
@click=${this.navLink}
>${name}</a
>
</li>`
)}
</ul>
`,
() => noneText
),
() => html`<sl-skeleton></sl-skeleton>`
)}
</btrix-desc-list-item>
</btrix-desc-list>
`;
}

View File

@ -129,6 +129,7 @@ export type Crawl = CrawlConfig & {
seedCount: number;
stopping: boolean;
collectionIds: string[];
collections: { id: string; name: string }[];
type?: "crawl" | "upload" | null;
};