From a1f12d369b71b596bdbe2e290236c1af30dfe816 Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Thu, 13 Feb 2025 11:18:07 -0500 Subject: [PATCH 1/9] wip --- frontend/src/pages/org/collections-list.ts | 101 ++++++++++++++++++--- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts index e8a788dc42..fc8526cf67 100644 --- a/frontend/src/pages/org/collections-list.ts +++ b/frontend/src/pages/org/collections-list.ts @@ -1,5 +1,10 @@ import { localized, msg } from "@lit/localize"; -import type { SlInput, SlMenuItem } from "@shoelace-style/shoelace"; +import type { + SlChangeEvent, + SlInput, + SlMenuItem, + SlRadioGroup, +} from "@shoelace-style/shoelace"; import Fuse from "fuse.js"; import { html, nothing, type PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; @@ -79,6 +84,11 @@ const sortableFields: Record< }; const MIN_SEARCH_LENGTH = 2; +enum ListView { + List = "list", + Grid = "grid", +} + @customElement("btrix-collections-list") @localized() export class CollectionsList extends BtrixElement { @@ -97,6 +107,9 @@ export class CollectionsList extends BtrixElement { direction: sortableFields["modified"].defaultDirection!, }; + @state() + private listView = ListView.List; + @state() private filterBy: Partial<Record<keyof Collection, unknown>> = {}; @@ -178,8 +191,13 @@ export class CollectionsList extends BtrixElement { > ${this.renderControls()} </div> - <div class="overflow-auto px-2 pb-1"> - ${guard([this.collections], this.renderList)} + <div class="-mx-3 overflow-auto px-3 pb-1"> + ${guard( + [this.collections, this.listView], + this.listView === ListView.List + ? this.renderList + : this.renderGrid, + )} </div> ` : this.renderLoading(), @@ -291,17 +309,45 @@ export class CollectionsList extends BtrixElement { `, )} </sl-select> - <sl-icon-button - name="arrow-down-up" - label=${msg("Reverse sort")} - @click=${() => { - this.orderBy = { - ...this.orderBy, - direction: -1 * this.orderBy.direction, - }; - }} - ></sl-icon-button> + <sl-tooltip content=${msg("Reverse sort")}> + <sl-icon-button + name="arrow-down-up" + label=${msg("Reverse sort")} + @click=${() => { + this.orderBy = { + ...this.orderBy, + direction: -1 * this.orderBy.direction, + }; + }} + ></sl-icon-button> + </sl-tooltip> </div> + <label for="viewStyle" class="mx-2 whitespace-nowrap text-neutral-500" + >${msg("View:")}</label + > + <sl-radio-group + id="viewStyle" + value=${this.listView} + size="small" + @sl-change=${(e: SlChangeEvent) => { + this.listView = (e.target as SlRadioGroup).value as ListView; + }} + > + <sl-tooltip content=${msg("View as List")}> + <sl-radio-button pill value=${ListView.List}> + <sl-icon + name="view-list" + label=${msg("List")} + ></sl-icon> </sl-radio-button + ></sl-tooltip> + <sl-tooltip content=${msg("View as Grid")}> + <sl-radio-button pill value=${ListView.Grid}> + <sl-icon + name="grid" + label=${msg("Grid")} + ></sl-icon> </sl-radio-button + ></sl-tooltip> + </sl-radio-group> </div> </div> `; @@ -386,6 +432,35 @@ export class CollectionsList extends BtrixElement { `; } + private readonly renderGrid = () => { + return html`<btrix-collections-grid + slug=${this.orgSlugState || ""} + .collections=${this.collections?.items} + > + ${this.collections && + this.collections.total > this.collections.items.length + ? html` + <btrix-pagination + page=${this.collections.page} + totalCount=${this.collections.total} + size=${this.collections.pageSize} + @page-change=${async (e: PageChangeEvent) => { + await this.fetchCollections({ + page: e.detail.page, + }); + + // Scroll to top of list + // TODO once deep-linking is implemented, scroll to top of pushstate + this.scrollIntoView({ behavior: "smooth" }); + }} + slot="pagination" + > + </btrix-pagination> + ` + : nothing} + </btrix-collections-grid>`; + }; + private readonly renderList = () => { if (this.collections?.items.length) { return html` From c7eb2e2e50185a973250e320900c459fc1519e6d Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Thu, 13 Feb 2025 11:30:29 -0500 Subject: [PATCH 2/9] handle refreshing collections a little better --- frontend/src/pages/org/collections-list.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts index fc8526cf67..d570fcd410 100644 --- a/frontend/src/pages/org/collections-list.ts +++ b/frontend/src/pages/org/collections-list.ts @@ -128,6 +128,9 @@ export class CollectionsList extends BtrixElement { @state() private selectedCollection?: Collection; + @state() + collectionRefreshing: string | null = null; + @state() private fetchErrorStatusCode?: number; @@ -193,7 +196,7 @@ export class CollectionsList extends BtrixElement { </div> <div class="-mx-3 overflow-auto px-3 pb-1"> ${guard( - [this.collections, this.listView], + [this.collections, this.listView, this.collectionRefreshing], this.listView === ListView.List ? this.renderList : this.renderGrid, @@ -436,6 +439,14 @@ export class CollectionsList extends BtrixElement { return html`<btrix-collections-grid slug=${this.orgSlugState || ""} .collections=${this.collections?.items} + .collectionRefreshing=${this.collectionRefreshing} + showVisibility + class="mt-8 block" + @btrix-collection-saved=${async ({ detail }: CollectionSavedEvent) => { + this.collectionRefreshing = detail.id; + await this.fetchCollections(); + this.collectionRefreshing = null; + }} > ${this.collections && this.collections.total > this.collections.items.length @@ -468,7 +479,7 @@ export class CollectionsList extends BtrixElement { class="[--btrix-column-gap:var(--sl-spacing-small)]" style="grid-template-columns: min-content [clickable-start] 45em repeat(4, 1fr) [clickable-end] min-content" > - <btrix-table-head class="mb-2 whitespace-nowrap"> + <btrix-table-head class="mb-2 mt-1 whitespace-nowrap"> <btrix-table-header-cell> <span class="sr-only">${msg("Collection Access")}</span> </btrix-table-header-cell> From 3d0fed71fab19597507133a692aa2e3c3729a65b Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Thu, 13 Feb 2025 11:35:42 -0500 Subject: [PATCH 3/9] make access icons more consistent with list --- .../features/collections/collections-grid.ts | 74 ++++++++++++++++--- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts index 989ebce01b..017384758e 100644 --- a/frontend/src/features/collections/collections-grid.ts +++ b/frontend/src/features/collections/collections-grid.ts @@ -7,6 +7,7 @@ import { queryAssignedNodes, state, } from "lit/decorators.js"; +import { choose } from "lit/directives/choose.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { when } from "lit/directives/when.js"; @@ -16,7 +17,7 @@ import { SelectCollectionAccess } from "./select-collection-access"; import { BtrixElement } from "@/classes/BtrixElement"; import { textSeparator } from "@/layouts/separator"; import { RouteNamespace } from "@/routes"; -import type { PublicCollection } from "@/types/collection"; +import { CollectionAccess, type PublicCollection } from "@/types/collection"; import { pluralOf } from "@/utils/pluralize"; import { tw } from "@/utils/tailwind"; @@ -107,15 +108,68 @@ export class CollectionsGrid extends BtrixElement { </div> <div class="${showActions ? "mr-9" : ""} min-h-9 leading-tight"> ${this.showVisibility - ? html`<sl-icon - class="mr-[5px] align-[-1px] text-sm" - name=${SelectCollectionAccess.Options[collection.access] - .icon} - label=${SelectCollectionAccess.Options[ - collection.access - ].label} - ></sl-icon>` - : nothing} + ? choose(collection.access, [ + [ + CollectionAccess.Private, + () => html` + <sl-tooltip + content=${SelectCollectionAccess.Options[ + CollectionAccess.Private + ].label} + > + <sl-icon + class="mr-[5px] inline-block align-[-1px] text-neutral-600" + name=${SelectCollectionAccess.Options[ + CollectionAccess.Private + ].icon} + ></sl-icon> + </sl-tooltip> + `, + ], + [ + CollectionAccess.Unlisted, + () => html` + <sl-tooltip + content=${SelectCollectionAccess.Options[ + CollectionAccess.Unlisted + ].label} + > + <sl-icon + class="mr-[5px] inline-block align-[-1px] text-neutral-600" + name=${SelectCollectionAccess.Options[ + CollectionAccess.Unlisted + ].icon} + ></sl-icon> + </sl-tooltip> + `, + ], + [ + CollectionAccess.Public, + () => html` + <sl-tooltip + content=${SelectCollectionAccess.Options[ + CollectionAccess.Public + ].label} + > + <sl-icon + class="mr-[5px] inline-block align-[-1px] text-success-600" + name=${SelectCollectionAccess.Options[ + CollectionAccess.Public + ].icon} + ></sl-icon> + </sl-tooltip> + `, + ], + ]) + : // ? html`<sl-icon + // class="mr-[5px] align-[-1px] text-sm" + // name=${SelectCollectionAccess.Options[collection.access] + // .icon} + // label=${SelectCollectionAccess.Options[ + // collection.access + // ].label} + // ></sl-icon>` + nothing} <strong class="text-base font-medium leading-tight text-stone-800 transition-colors group-hover:text-cyan-600" > From 714bc2b624ed9609113c415655c92cbd3575009e Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Thu, 17 Apr 2025 15:06:55 -0400 Subject: [PATCH 4/9] refactor collection editing dialog into separate component this makes collection grid simpler: it doesn't include extra editing dialog when not needed Extract collection edit dialog into dedicated component - Move dialog from collections-grid to collections-grid-with-edit-dialog - Update dashboard page to use new composite component --- .../collections-grid-with-edit-dialog.ts | 52 ++++++++ .../features/collections/collections-grid.ts | 28 +---- frontend/src/features/collections/index.ts | 1 + frontend/src/pages/org/dashboard.ts | 116 ++++++++++-------- 4 files changed, 121 insertions(+), 76 deletions(-) create mode 100644 frontend/src/features/collections/collections-grid-with-edit-dialog.ts diff --git a/frontend/src/features/collections/collections-grid-with-edit-dialog.ts b/frontend/src/features/collections/collections-grid-with-edit-dialog.ts new file mode 100644 index 0000000000..3623aae104 --- /dev/null +++ b/frontend/src/features/collections/collections-grid-with-edit-dialog.ts @@ -0,0 +1,52 @@ +import { localized } from "@lit/localize"; +import { html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { when } from "lit/directives/when.js"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import { type PublicCollection } from "@/types/collection"; + +@customElement("btrix-collections-grid-with-edit-dialog") +@localized() +export class CollectionsGridWithEditDialog extends BtrixElement { + @property({ type: Array }) + collections?: PublicCollection[]; + + @state() + collectionBeingEdited: string | null = null; + + @property({ type: String }) + collectionRefreshing: string | null = null; + + @property({ type: Boolean }) + showVisibility = false; + + render() { + const showActions = !this.navigate.isPublicPage && this.appState.isCrawler; + return html` + <btrix-collections-grid + slug=${this.orgSlugState || ""} + .collections=${this.collections} + .collectionRefreshing=${this.collectionRefreshing} + ?showVisibility=${this.showVisibility} + @btrix-edit-collection=${(e: CustomEvent<string>) => { + this.collectionBeingEdited = e.detail; + }} + > + <slot name="empty-actions" slot="empty-actions"></slot> + <slot name="pagination" slot="pagination"></slot> + </btrix-collections-grid> + ${when( + showActions, + () => + html`<btrix-collection-edit-dialog + .collectionId=${this.collectionBeingEdited ?? undefined} + ?open=${!!this.collectionBeingEdited} + @sl-after-hide=${() => { + this.collectionBeingEdited = null; + }} + ></btrix-collection-edit-dialog>`, + )} + `; + } +} diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts index 017384758e..93dce950d3 100644 --- a/frontend/src/features/collections/collections-grid.ts +++ b/frontend/src/features/collections/collections-grid.ts @@ -1,12 +1,7 @@ import { localized, msg } from "@lit/localize"; import clsx from "clsx"; import { html, nothing } from "lit"; -import { - customElement, - property, - queryAssignedNodes, - state, -} from "lit/decorators.js"; +import { customElement, property, queryAssignedNodes } from "lit/decorators.js"; import { choose } from "lit/directives/choose.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { when } from "lit/directives/when.js"; @@ -35,9 +30,6 @@ export class CollectionsGrid extends BtrixElement { @property({ type: Array }) collections?: PublicCollection[]; - @state() - collectionBeingEdited: string | null = null; - @property({ type: String }) collectionRefreshing: string | null = null; @@ -212,18 +204,6 @@ export class CollectionsGrid extends BtrixElement { class=${clsx("justify-center flex", this.pagination.length && "mt-10")} name="pagination" ></slot> - - ${when( - showActions, - () => - html`<btrix-collection-edit-dialog - .collectionId=${this.collectionBeingEdited ?? undefined} - ?open=${!!this.collectionBeingEdited} - @sl-after-hide=${() => { - this.collectionBeingEdited = null; - }} - ></btrix-collection-edit-dialog>`, - )} `; } @@ -235,7 +215,11 @@ export class CollectionsGrid extends BtrixElement { raised size="small" @click=${() => { - this.collectionBeingEdited = collection.id; + this.dispatchEvent( + new CustomEvent<string>("btrix-edit-collection", { + detail: collection.id, + }), + ); }} > <sl-icon name="pencil"></sl-icon> diff --git a/frontend/src/features/collections/index.ts b/frontend/src/features/collections/index.ts index 2ed846c3b7..a4fbc66b0a 100644 --- a/frontend/src/features/collections/index.ts +++ b/frontend/src/features/collections/index.ts @@ -1,5 +1,6 @@ import("./collections-add"); import("./collections-grid"); +import("./collections-grid-with-edit-dialog"); import("./collection-items-dialog"); import("./collection-edit-dialog"); import("./collection-create-dialog"); diff --git a/frontend/src/pages/org/dashboard.ts b/frontend/src/pages/org/dashboard.ts index 3354cec1ca..10fc0441d4 100644 --- a/frontend/src/pages/org/dashboard.ts +++ b/frontend/src/pages/org/dashboard.ts @@ -376,33 +376,35 @@ export class Dashboard extends BtrixElement { ? msg("Public Collections") : msg("All Collections"), })} - ${this.collectionsView === CollectionGridView.Public - ? html` <span class="text-sm text-neutral-400" - >— - <a - href=${`/${RouteNamespace.PublicOrgs}/${this.orgSlugState}`} - class="inline-flex h-8 items-center text-sm font-medium text-primary-500 transition hover:text-primary-600" - @click=${this.navigate.link} - > + ${ + this.collectionsView === CollectionGridView.Public + ? html` <span class="text-sm text-neutral-400" + >— + <a + href=${`/${RouteNamespace.PublicOrgs}/${this.orgSlugState}`} + class="inline-flex h-8 items-center text-sm font-medium text-primary-500 transition hover:text-primary-600" + @click=${this.navigate.link} + > + ${this.org?.enablePublicProfile + ? msg("Visit public collections gallery") + : msg("Preview public collections gallery")} + </a> + <!-- TODO Refactor clipboard code, get URL in a nicer way? --> ${this.org?.enablePublicProfile - ? msg("Visit public collections gallery") - : msg("Preview public collections gallery")} - </a> - <!-- TODO Refactor clipboard code, get URL in a nicer way? --> - ${this.org?.enablePublicProfile - ? html`<btrix-copy-button - value=${new URL( - `/${RouteNamespace.PublicOrgs}/${this.orgSlugState}`, - window.location.toString(), - ).toString()} - content=${msg( - "Copy Link to Public Collections Gallery", - )} - class="inline-block" - ></btrix-copy-button>` - : nothing} - </span>` - : nothing} + ? html`<btrix-copy-button + value=${new URL( + `/${RouteNamespace.PublicOrgs}/${this.orgSlugState}`, + window.location.toString(), + ).toString()} + content=${msg( + "Copy Link to Public Collections Gallery", + )} + class="inline-block" + ></btrix-copy-button>` + : nothing} + </span>` + : nothing + } </div> <div class="flex items-center gap-2"> ${when( @@ -446,8 +448,7 @@ export class Dashboard extends BtrixElement { </div> </header> <div class="relative rounded-lg border p-10"> - <btrix-collections-grid - slug=${this.orgSlugState || ""} + <btrix-collections-grid-with-edit-dialog .collections=${this.collections.value?.items} .collectionRefreshing=${this.collectionRefreshing} ?showVisibility=${this.collectionsView === CollectionGridView.All} @@ -463,34 +464,41 @@ export class Dashboard extends BtrixElement { > ${this.renderNoPublicCollections()} <span slot="empty-text" - >${this.collectionsView === CollectionGridView.Public - ? msg("No public collections yet.") - : msg("No collections yet.")}</span + >${ + this.collectionsView === CollectionGridView.Public + ? msg("No public collections yet.") + : msg("No collections yet.") + }</span > - ${this.collections.value && - this.collections.value.total > this.collections.value.items.length - ? html` - <btrix-pagination - page=${this.collectionPage} - size=${PAGE_SIZE} - totalCount=${this.collections.value.total} - @page-change=${(e: PageChangeEvent) => { - this.collectionPage = e.detail.page; - }} - slot="pagination" - > - </btrix-pagination> - ` - : nothing} + ${ + this.collections.value && + this.collections.value.total > + this.collections.value.items.length + ? html` + <btrix-pagination + page=${this.collectionPage} + size=${PAGE_SIZE} + totalCount=${this.collections.value.total} + @page-change=${(e: PageChangeEvent) => { + this.collectionPage = e.detail.page; + }} + slot="pagination" + > + </btrix-pagination> + ` + : nothing + } </btrix-collections-grid> - ${this.collections.status === TaskStatus.PENDING && - this.collections.value - ? html`<div - class="absolute inset-0 rounded-lg bg-stone-50/75 p-24 text-center text-4xl" - > - <sl-spinner></sl-spinner> - </div>` - : nothing} + ${ + this.collections.status === TaskStatus.PENDING && + this.collections.value + ? html`<div + class="absolute inset-0 rounded-lg bg-stone-50/75 p-24 text-center text-4xl" + > + <sl-spinner></sl-spinner> + </div>` + : nothing + } </div> </section> </main> From 022141eb7b3a51f0a0fe63e411eed14bc9fbebc6 Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Thu, 17 Apr 2025 15:21:22 -0400 Subject: [PATCH 5/9] add getter/setter for page in pagination component --- frontend/src/components/ui/pagination.ts | 63 +++++++++++++++--------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/ui/pagination.ts b/frontend/src/components/ui/pagination.ts index af57c7b34e..59a88256cd 100644 --- a/frontend/src/components/ui/pagination.ts +++ b/frontend/src/components/ui/pagination.ts @@ -141,19 +141,30 @@ export class Pagination extends LitElement { searchParams = new SearchParamsController(this, (params) => { const page = parsePage(params.get(this.name)); - if (this.page !== page) { + if (this._page !== page) { this.dispatchEvent( new CustomEvent<PageChangeDetail>("page-change", { detail: { page: page, pages: this.pages }, composed: true, }), ); - this.page = page; + this._page = page; } }); @state() - page = 1; + private _page = 1; + + @property({ type: Number }) + set page(page: number) { + if (page !== this._page) { + this.setPage(page); + } + } + + get page() { + return this._page; + } @property({ type: String }) name = "page"; @@ -174,7 +185,7 @@ export class Pagination extends LitElement { private pages = 0; connectedCallback() { - this.inputValue = `${this.page}`; + this.inputValue = `${this._page}`; super.connectedCallback(); } @@ -186,14 +197,14 @@ export class Pagination extends LitElement { const parsedPage = parseFloat( this.searchParams.searchParams.get(this.name) ?? "1", ); - if (parsedPage != this.page) { + if (parsedPage != this._page) { const page = parsePage(this.searchParams.searchParams.get(this.name)); const constrainedPage = Math.max(1, Math.min(this.pages, page)); this.onPageChange(constrainedPage); } - if (changedProperties.get("page") && this.page) { - this.inputValue = `${this.page}`; + if (changedProperties.get("page") && this._page) { + this.inputValue = `${this._page}`; } } @@ -208,7 +219,7 @@ export class Pagination extends LitElement { <li> <button class="navButton" - ?disabled=${this.page === 1} + ?disabled=${this._page === 1} @click=${this.onPrev} > <img class="chevron" src=${chevronLeft} /> @@ -221,7 +232,7 @@ export class Pagination extends LitElement { <li> <button class="navButton" - ?disabled=${this.page === this.pages} + ?disabled=${this._page === this.pages} @click=${this.onNext} > <span class=${classMap({ srOnly: this.compact })} @@ -250,7 +261,7 @@ export class Pagination extends LitElement { inputmode="numeric" size="small" value=${this.inputValue} - aria-label=${msg(str`Current page, page ${this.page}`)} + aria-label=${msg(str`Current page, page ${this._page}`)} aria-current="page" autocomplete="off" min="1" @@ -302,7 +313,7 @@ export class Pagination extends LitElement { const middleEnd = middleVisible * 2 - 1; const endsVisible = 2; if (this.pages > middleVisible + middleEnd) { - const currentPageIdx = pages.indexOf(this.page); + const currentPageIdx = pages.indexOf(this._page); const firstPages = pages.slice(0, endsVisible); const lastPages = pages.slice(-1 * endsVisible); let middlePages = pages.slice(endsVisible, middleEnd); @@ -331,7 +342,7 @@ export class Pagination extends LitElement { }; private readonly renderPageButton = (page: number) => { - const isCurrent = page === this.page; + const isCurrent = page === this._page; return html`<li aria-current=${ifDefined(isCurrent ? "page" : undefined)}> <btrix-navigation-button icon @@ -346,23 +357,16 @@ export class Pagination extends LitElement { }; private onPrev() { - this.onPageChange(this.page > 1 ? this.page - 1 : 1); + this.onPageChange(this._page > 1 ? this._page - 1 : 1); } private onNext() { - this.onPageChange(this.page < this.pages ? this.page + 1 : this.pages); + this.onPageChange(this._page < this.pages ? this._page + 1 : this.pages); } private onPageChange(page: number) { - if (this.page !== page) { - this.searchParams.set((params) => { - if (page === 1) { - params.delete(this.name); - } else { - params.set(this.name, page.toString()); - } - return params; - }); + if (this._page !== page) { + this.setPage(page); this.dispatchEvent( new CustomEvent<PageChangeDetail>("page-change", { detail: { page: page, pages: this.pages }, @@ -370,7 +374,18 @@ export class Pagination extends LitElement { }), ); } - this.page = page; + this._page = page; + } + + private setPage(page: number) { + this.searchParams.set((params) => { + if (page === 1) { + params.delete(this.name); + } else { + params.set(this.name, page.toString()); + } + return params; + }); } private calculatePages() { From 835dd0a138c94ce8478daf4ef82ebd0693253a77 Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Thu, 17 Apr 2025 15:22:12 -0400 Subject: [PATCH 6/9] key collection thumbnails to collection ids to prevent flash of previous thumbnail when switching pages --- .../features/collections/collections-grid.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts index 93dce950d3..be55e54f02 100644 --- a/frontend/src/features/collections/collections-grid.ts +++ b/frontend/src/features/collections/collections-grid.ts @@ -4,6 +4,7 @@ import { html, nothing } from "lit"; import { customElement, property, queryAssignedNodes } from "lit/decorators.js"; import { choose } from "lit/directives/choose.js"; import { ifDefined } from "lit/directives/if-defined.js"; +import { keyed } from "lit/directives/keyed.js"; import { when } from "lit/directives/when.js"; import { CollectionThumbnail } from "./collection-thumbnail"; @@ -88,14 +89,24 @@ export class CollectionsGrid extends BtrixElement { <div class="relative mb-4 rounded-lg shadow-md shadow-stone-600/10 ring-1 ring-stone-600/10 transition group-hover:shadow-stone-800/20 group-hover:ring-stone-800/20" > - <btrix-collection-thumbnail - src=${ifDefined( - Object.entries(CollectionThumbnail.Variants).find( - ([name]) => name === collection.defaultThumbnailName, - )?.[1].path || collection.thumbnail?.path, - )} - collectionName=${collection.name} - ></btrix-collection-thumbnail> + ${ + // When swapping images, the previous image is retained until the new one is loaded, + // which leads to the wrong image briefly being displayed when switching pages. + // This removes and replaces the image instead, which prevents this at the cost of the + // occasional flash of white while loading, but overall this feels more responsive. + keyed( + collection.id, + html` <btrix-collection-thumbnail + src=${ifDefined( + Object.entries(CollectionThumbnail.Variants).find( + ([name]) => + name === collection.defaultThumbnailName, + )?.[1].path || collection.thumbnail?.path, + )} + collectionName=${collection.name} + ></btrix-collection-thumbnail>`, + ) + } ${this.renderDateBadge(collection)} </div> <div class="${showActions ? "mr-9" : ""} min-h-9 leading-tight"> From d7279789b161801a454961bae32f3183f27ff95f Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Thu, 17 Apr 2025 15:39:44 -0400 Subject: [PATCH 7/9] add full action menu to collection list grid items --- .../src/components/ui/overflow-dropdown.ts | 5 ++- .../features/collections/collections-grid.ts | 41 +++++++++++-------- frontend/src/pages/org/collections-list.ts | 12 +++++- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/ui/overflow-dropdown.ts b/frontend/src/components/ui/overflow-dropdown.ts index 1bc0ea8f78..fa9446924a 100644 --- a/frontend/src/components/ui/overflow-dropdown.ts +++ b/frontend/src/components/ui/overflow-dropdown.ts @@ -31,6 +31,9 @@ export class OverflowDropdown extends TailwindElement { @property({ type: Boolean }) raised = false; + @property({ type: String }) + size?: "x-small" | "small" | "medium"; + @state() private hasMenuItems?: boolean; @@ -47,7 +50,7 @@ export class OverflowDropdown extends TailwindElement { hoist distance=${ifDefined(this.raised ? "4" : undefined)} > - <btrix-button slot="trigger" ?raised=${this.raised}> + <btrix-button slot="trigger" ?raised=${this.raised} size=${this.size}> <sl-icon label=${msg("Actions")} name="three-dots-vertical" diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts index be55e54f02..f8a8ec5273 100644 --- a/frontend/src/features/collections/collections-grid.ts +++ b/frontend/src/features/collections/collections-grid.ts @@ -1,6 +1,6 @@ import { localized, msg } from "@lit/localize"; import clsx from "clsx"; -import { html, nothing } from "lit"; +import { html, nothing, type TemplateResult } from "lit"; import { customElement, property, queryAssignedNodes } from "lit/decorators.js"; import { choose } from "lit/directives/choose.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -37,6 +37,9 @@ export class CollectionsGrid extends BtrixElement { @property({ type: Boolean }) showVisibility = false; + @property() + renderActions?: (collection: PublicCollection) => TemplateResult; + @queryAssignedNodes({ slot: "pagination" }) pagination!: Node[]; @@ -196,7 +199,7 @@ export class CollectionsGrid extends BtrixElement { `} </div> </a> - ${when(showActions, () => this.renderActions(collection))} + ${when(showActions, () => this._renderActions(collection))} ${when( this.collectionRefreshing === collection.id, () => @@ -218,24 +221,26 @@ export class CollectionsGrid extends BtrixElement { `; } - private readonly renderActions = (collection: PublicCollection) => html` + private readonly _renderActions = (collection: PublicCollection) => html` <div class="pointer-events-none absolute left-0 right-0 top-0 aspect-video"> <div class="pointer-events-auto absolute bottom-2 right-2"> - <sl-tooltip content=${msg("Edit Collection Settings")}> - <btrix-button - raised - size="small" - @click=${() => { - this.dispatchEvent( - new CustomEvent<string>("btrix-edit-collection", { - detail: collection.id, - }), - ); - }} - > - <sl-icon name="pencil"></sl-icon> - </btrix-button> - </sl-tooltip> + ${this.renderActions + ? this.renderActions(collection) + : html`<sl-tooltip content=${msg("Edit Collection Settings")}> + <btrix-button + raised + size="small" + @click=${() => { + this.dispatchEvent( + new CustomEvent<string>("btrix-edit-collection", { + detail: collection.id, + }), + ); + }} + > + <sl-icon name="pencil"></sl-icon> + </btrix-button> + </sl-tooltip>`} </div> </div> `; diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts index d570fcd410..c2762de76a 100644 --- a/frontend/src/pages/org/collections-list.ts +++ b/frontend/src/pages/org/collections-list.ts @@ -440,6 +440,8 @@ export class CollectionsList extends BtrixElement { slug=${this.orgSlugState || ""} .collections=${this.collections?.items} .collectionRefreshing=${this.collectionRefreshing} + .renderActions=${(col: Collection) => + this.renderActions(col, { renderOnGridItem: true })} showVisibility class="mt-8 block" @btrix-collection-saved=${async ({ detail }: CollectionSavedEvent) => { @@ -661,11 +663,17 @@ export class CollectionsList extends BtrixElement { </btrix-table-row> `; - private readonly renderActions = (col: Collection) => { + private readonly renderActions = ( + col: Collection, + { renderOnGridItem } = { renderOnGridItem: false }, + ) => { const authToken = this.authState?.headers.Authorization.split(" ")[1]; return html` - <btrix-overflow-dropdown> + <btrix-overflow-dropdown + ?raised=${renderOnGridItem} + size=${renderOnGridItem ? "small" : "medium"} + > <sl-menu> <sl-menu-item @click=${() => void this.manageCollection(col, "edit")}> <sl-icon name="gear" slot="prefix"></sl-icon> From 31ef5a0832349fb50a56dc0f72573a9599aad0b8 Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Thu, 17 Apr 2025 15:54:28 -0400 Subject: [PATCH 8/9] simplify collection visibility icon rendering --- .../features/collections/collections-grid.ts | 81 +++++-------------- 1 file changed, 18 insertions(+), 63 deletions(-) diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts index f8a8ec5273..0add88e1d6 100644 --- a/frontend/src/features/collections/collections-grid.ts +++ b/frontend/src/features/collections/collections-grid.ts @@ -2,7 +2,6 @@ import { localized, msg } from "@lit/localize"; import clsx from "clsx"; import { html, nothing, type TemplateResult } from "lit"; import { customElement, property, queryAssignedNodes } from "lit/decorators.js"; -import { choose } from "lit/directives/choose.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { keyed } from "lit/directives/keyed.js"; import { when } from "lit/directives/when.js"; @@ -114,68 +113,24 @@ export class CollectionsGrid extends BtrixElement { </div> <div class="${showActions ? "mr-9" : ""} min-h-9 leading-tight"> ${this.showVisibility - ? choose(collection.access, [ - [ - CollectionAccess.Private, - () => html` - <sl-tooltip - content=${SelectCollectionAccess.Options[ - CollectionAccess.Private - ].label} - > - <sl-icon - class="mr-[5px] inline-block align-[-1px] text-neutral-600" - name=${SelectCollectionAccess.Options[ - CollectionAccess.Private - ].icon} - ></sl-icon> - </sl-tooltip> - `, - ], - [ - CollectionAccess.Unlisted, - () => html` - <sl-tooltip - content=${SelectCollectionAccess.Options[ - CollectionAccess.Unlisted - ].label} - > - <sl-icon - class="mr-[5px] inline-block align-[-1px] text-neutral-600" - name=${SelectCollectionAccess.Options[ - CollectionAccess.Unlisted - ].icon} - ></sl-icon> - </sl-tooltip> - `, - ], - [ - CollectionAccess.Public, - () => html` - <sl-tooltip - content=${SelectCollectionAccess.Options[ - CollectionAccess.Public - ].label} - > - <sl-icon - class="mr-[5px] inline-block align-[-1px] text-success-600" - name=${SelectCollectionAccess.Options[ - CollectionAccess.Public - ].icon} - ></sl-icon> - </sl-tooltip> - `, - ], - ]) - : // ? html`<sl-icon - // class="mr-[5px] align-[-1px] text-sm" - // name=${SelectCollectionAccess.Options[collection.access] - // .icon} - // label=${SelectCollectionAccess.Options[ - // collection.access - // ].label} - // ></sl-icon>` - nothing} + ? html`<sl-tooltip + content=${SelectCollectionAccess.Options[ + collection.access + ].label} + > + <sl-icon + class=${clsx( + "mr-[5px] inline-block align-[-1px]", + collection.access === CollectionAccess.Public + ? "text-success-600" + : "text-neutral-600", + )} + name=${SelectCollectionAccess.Options[ + collection.access + ].icon} + ></sl-icon> + </sl-tooltip>` + : nothing} <strong class="text-base font-medium leading-tight text-stone-800 transition-colors group-hover:text-cyan-600" > From fe44872c571ba8b300fb7f2b442aed6ea07a4535 Mon Sep 17 00:00:00 2001 From: emma <hi@emma.cafe> Date: Mon, 21 Apr 2025 21:48:17 -0400 Subject: [PATCH 9/9] add description for `collectionRefreshing` property --- frontend/src/pages/org/collections-list.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts index c2762de76a..a9fc0e3bec 100644 --- a/frontend/src/pages/org/collections-list.ts +++ b/frontend/src/pages/org/collections-list.ts @@ -128,8 +128,9 @@ export class CollectionsList extends BtrixElement { @state() private selectedCollection?: Collection; + /** ID of the collection currently being refreshed */ @state() - collectionRefreshing: string | null = null; + private collectionRefreshing: string | null = null; @state() private fetchErrorStatusCode?: number;