import { localized, msg, str } from "@lit/localize"; import type { SlChangeEvent, SlSelect } from "@shoelace-style/shoelace"; import { html, type PropertyValues } from "lit"; import { customElement, property, query } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js"; import { TailwindElement } from "@/classes/TailwindElement"; import { type PageChangeEvent } from "@/components/ui/pagination"; import type { APIPaginatedList, APISortQuery } from "@/types/api"; import type { ArchivedItemQAPage } from "@/types/qa"; export type SortDirection = "asc" | "desc"; export type SortableFieldNames = | "textMatch" | "screenshotMatch" | "approved" | "notes"; type SortableFields = Record< SortableFieldNames, { label: string; defaultDirection?: SortDirection; } >; const sortableFields = { textMatch: { label: msg("Text Match"), defaultDirection: "asc", }, screenshotMatch: { label: msg("Screenshot Match"), defaultDirection: "desc", }, approved: { label: msg("Approval"), // defaultDirection: "asc", }, notes: { label: msg("Comments"), defaultDirection: "desc", }, // url: { // label: msg("Page URL"), // defaultDirection: "desc", // }, // title: { // label: msg("Page Title"), // defaultDirection: "desc", // }, // timestamp: { // label: msg("Time"), // defaultDirection: "asc", // }, } satisfies SortableFields; type SortField = keyof typeof sortableFields; export type OrderBy = { field: SortField; direction: SortDirection; }; export type QaPaginationChangeDetail = { page: number; }; export type QaFilterChangeDetail = { reviewed: undefined | boolean; approved: undefined | boolean; hasNotes: undefined | boolean; }; export type QaSortChangeDetail = APISortQuery & { sortBy: SortableFieldNames }; /** * @fires btrix-qa-pagination-change * @fires btrix-qa-filter-change * @fires btrix-qa-sort-change */ @localized() @customElement("btrix-qa-page-list") export class PageList extends TailwindElement { @property({ type: String }) qaRunId?: string; @property({ type: String }) itemPageId?: string; @property({ type: Object }) pages?: APIPaginatedList; @property({ type: Number }) totalPages = 0; @property({ type: Object }) orderBy: OrderBy = { field: "screenshotMatch", direction: "asc", }; @property({ type: Object }) filterBy: { reviewed?: boolean; approved?: boolean; hasNotes?: boolean; } = {}; @query(".scrollContainer") private readonly scrollContainer?: HTMLElement | null; protected async updated(changedProperties: PropertyValues) { if ( changedProperties.has("pages") && changedProperties.get("pages") && this.pages ) { this.scrollContainer?.scrollTo({ top: 0, left: 0 }); } } render() { return html`
${this.renderSortControl()} ${this.renderFilterControl()}
${this.pages?.total ? html`
${this.pages.total === this.totalPages ? msg( str`Showing all ${this.totalPages.toLocaleString()} pages`, ) : msg( str`Showing ${this.pages.total.toLocaleString()} of ${this.totalPages.toLocaleString()} pages`, )}
${repeat( this.pages.items, ({ id }) => id, (page: ArchivedItemQAPage) => html` `, )}
{ e.stopPropagation(); this.dispatchEvent( new CustomEvent( "btrix-qa-pagination-change", { detail: { page: e.detail.page }, }, ), ); }} >
` : html`
${msg("No matching pages found")}
`}
`; } private renderSortControl() { return html`
{ const { value } = e.target as SlSelect; const detail: QaSortChangeDetail = { sortBy: this.orderBy.field, sortDirection: this.orderBy.direction === "asc" ? 1 : -1, }; switch (value) { case "bestScreenshotMatch": detail.sortBy = "screenshotMatch"; detail.sortDirection = -1; break; case "worstScreenshotMatch": detail.sortBy = "screenshotMatch"; detail.sortDirection = 1; break; case "bestTextMatch": detail.sortBy = "textMatch"; detail.sortDirection = -1; break; case "worstTextMatch": detail.sortBy = "textMatch"; detail.sortDirection = 1; break; case "approved": detail.sortBy = "approved"; detail.sortDirection = -1; break; case "notApproved": detail.sortBy = "approved"; detail.sortDirection = 1; break; case "comments": detail.sortBy = "notes"; detail.sortDirection = -1; break; // case "url": // detail.sortBy = "url"; // detail.sortDirection = 1; // break; // case "title": // detail.sortBy = "title"; // detail.sortDirection = 1; // break; default: break; } this.dispatchEvent( new CustomEvent("btrix-qa-sort-change", { detail, }), ); }} > ${msg("Best Screenshot Match")} ${msg("Worst Screenshot Match")} ${msg("Best Extracted Text Match")} ${msg("Worst Extracted Text Match")} ${msg("Most Comments")} ${msg("Recently Approved")} ${msg("Not Approved")}
`; } private renderFilterControl() { const value = () => { if (this.filterBy.approved) return "approved"; if (this.filterBy.approved === false) return "rejected"; if (this.filterBy.reviewed) return "reviewed"; if (this.filterBy.reviewed === false) return "notReviewed"; if (this.filterBy.hasNotes) return "hasNotes"; return ""; }; return html`
{ const { value } = e.target as SlSelect; const detail: QaFilterChangeDetail = { reviewed: undefined, approved: undefined, hasNotes: undefined, }; switch (value) { case "notReviewed": detail.reviewed = false; break; case "reviewed": detail.reviewed = true; break; case "approved": detail.approved = true; break; case "rejected": detail.approved = false; break; case "hasNotes": detail.hasNotes = true; break; default: break; } this.dispatchEvent( new CustomEvent("btrix-qa-filter-change", { detail, }), ); }} pill size="small" > ${msg("Any")} ${msg("None")} ${msg("Approved, Rejected, or Commented")} ${msg("Approved")} ${msg("Rejected")} ${msg("Commented")}
`; } }