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

View File

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

View File

@ -750,6 +750,32 @@ ${this.crawl?.description}
() => html`<sl-skeleton></sl-skeleton>` () => html`<sl-skeleton></sl-skeleton>`
)} )}
</btrix-desc-list-item> </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> </btrix-desc-list>
`; `;
} }

View File

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