Skip to content

Add grid view to collection list #2403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 23, 2025
5 changes: 4 additions & 1 deletion frontend/src/components/ui/overflow-dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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"
Expand Down
63 changes: 39 additions & 24 deletions frontend/src/components/ui/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -174,7 +185,7 @@ export class Pagination extends LitElement {
private pages = 0;

connectedCallback() {
this.inputValue = `${this.page}`;
this.inputValue = `${this._page}`;
super.connectedCallback();
}

Expand All @@ -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}`;
}
}

Expand All @@ -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} />
Expand All @@ -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 })}
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -346,31 +357,35 @@ 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 },
composed: true,
}),
);
}
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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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>`,
)}
`;
}
}
109 changes: 59 additions & 50 deletions frontend/src/features/collections/collections-grid.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
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 { html, nothing, type TemplateResult } from "lit";
import { customElement, property, queryAssignedNodes } from "lit/decorators.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";
Expand All @@ -16,7 +12,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";

Expand All @@ -34,15 +30,15 @@ export class CollectionsGrid extends BtrixElement {
@property({ type: Array })
collections?: PublicCollection[];

@state()
collectionBeingEdited: string | null = null;

@property({ type: String })
collectionRefreshing: string | null = null;

@property({ type: Boolean })
showVisibility = false;

@property()
renderActions?: (collection: PublicCollection) => TemplateResult;

@queryAssignedNodes({ slot: "pagination" })
pagination!: Node[];

Expand Down Expand Up @@ -95,26 +91,45 @@ 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">
${this.showVisibility
? html`<sl-icon
class="mr-[5px] align-[-1px] text-sm"
name=${SelectCollectionAccess.Options[collection.access]
.icon}
label=${SelectCollectionAccess.Options[
? html`<sl-tooltip
content=${SelectCollectionAccess.Options[
collection.access
].label}
></sl-icon>`
>
<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"
Expand All @@ -139,7 +154,7 @@ export class CollectionsGrid extends BtrixElement {
`}
</div>
</a>
${when(showActions, () => this.renderActions(collection))}
${when(showActions, () => this._renderActions(collection))}
${when(
this.collectionRefreshing === collection.id,
() =>
Expand All @@ -158,35 +173,29 @@ 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>`,
)}
`;
}

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.collectionBeingEdited = 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>
`;
Expand Down
Loading
Loading