feat: User-sort browser profiles list (#1839)
Resolves https://github.com/webrecorder/browsertrix/issues/1409 ### Changes - Enables clicking on Browser Profiles column header to sort the table, including by starting URL - More consistent column widths throughout app --------- Co-authored-by: Tessa Walsh <tessa@bitarchivist.net> Co-authored-by: emma <hi@emma.cafe> Co-authored-by: Henry Wilkinson <henry@wilkinson.graphics>
This commit is contained in:
		
							parent
							
								
									c2ce96c057
								
							
						
					
					
						commit
						4d4c8a04d4
					
				| @ -287,11 +287,14 @@ class ProfileOps: | ||||
|         aggregate: List[Dict[str, Any]] = [{"$match": match_query}] | ||||
| 
 | ||||
|         if sort_by: | ||||
|             if sort_by not in ("modified", "created", "name"): | ||||
|             if sort_by not in ("modified", "created", "name", "url"): | ||||
|                 raise HTTPException(status_code=400, detail="invalid_sort_by") | ||||
|             if sort_direction not in (1, -1): | ||||
|                 raise HTTPException(status_code=400, detail="invalid_sort_direction") | ||||
| 
 | ||||
|             if sort_by == "url": | ||||
|                 sort_by = "origins.0" | ||||
| 
 | ||||
|             aggregate.extend([{"$sort": {sort_by: sort_direction}}]) | ||||
| 
 | ||||
|         aggregate.extend( | ||||
|  | ||||
| @ -512,7 +512,9 @@ def profile_browser_id(admin_auth_headers, default_org_id): | ||||
| 
 | ||||
| @pytest.fixture(scope="session") | ||||
| def profile_browser_2_id(admin_auth_headers, default_org_id): | ||||
|     return _create_profile_browser(admin_auth_headers, default_org_id) | ||||
|     return _create_profile_browser( | ||||
|         admin_auth_headers, default_org_id, "https://specs.webrecorder.net" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture(scope="session") | ||||
| @ -520,11 +522,13 @@ def profile_browser_3_id(admin_auth_headers, default_org_id): | ||||
|     return _create_profile_browser(admin_auth_headers, default_org_id) | ||||
| 
 | ||||
| 
 | ||||
| def _create_profile_browser(headers: Dict[str, str], oid: UUID): | ||||
| def _create_profile_browser( | ||||
|     headers: Dict[str, str], oid: UUID, url: str = "https://webrecorder.net" | ||||
| ): | ||||
|     r = requests.post( | ||||
|         f"{API_PREFIX}/orgs/{oid}/profiles/browser", | ||||
|         headers=headers, | ||||
|         json={"url": "https://webrecorder.net"}, | ||||
|         json={"url": url}, | ||||
|     ) | ||||
|     assert r.status_code == 200 | ||||
|     browser_id = r.json()["browserid"] | ||||
|  | ||||
| @ -432,6 +432,10 @@ def test_commit_browser_to_existing_profile( | ||||
|         ("name", -1, 0, 1), | ||||
|         # Name, ascending | ||||
|         ("name", 1, 1, 0), | ||||
|         # URL, descending | ||||
|         ("url", -1, 0, 1), | ||||
|         # URL, ascending | ||||
|         ("url", 1, 1, 0), | ||||
|     ], | ||||
| ) | ||||
| def test_sort_profiles( | ||||
|  | ||||
| @ -1,10 +1,12 @@ | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { css, html } from "lit"; | ||||
| import { | ||||
|   customElement, | ||||
|   property, | ||||
|   queryAssignedElements, | ||||
| } from "lit/decorators.js"; | ||||
| 
 | ||||
| import { TailwindElement } from "@/classes/TailwindElement"; | ||||
| 
 | ||||
| export const ALLOWED_ROW_CLICK_TARGET_TAG = ["a", "label"] as const; | ||||
| 
 | ||||
| /** | ||||
| @ -27,7 +29,7 @@ export const ALLOWED_ROW_CLICK_TARGET_TAG = ["a", "label"] as const; | ||||
|  * @cssproperty --btrix-cell-padding-bottom | ||||
|  */ | ||||
| @customElement("btrix-table-cell") | ||||
| export class TableCell extends LitElement { | ||||
| export class TableCell extends TailwindElement { | ||||
|   static styles = css` | ||||
|     :host { | ||||
|       display: flex; | ||||
|  | ||||
| @ -3,15 +3,23 @@ import { customElement, property } from "lit/decorators.js"; | ||||
| 
 | ||||
| import { TableCell } from "./table-cell"; | ||||
| 
 | ||||
| import type { SortDirection as Direction } from "@/types/utils"; | ||||
| 
 | ||||
| export type SortValues = "ascending" | "descending" | "none"; | ||||
| export const SortDirection = new Map<Direction, SortValues>([ | ||||
|   [-1, "descending"], | ||||
|   [1, "ascending"], | ||||
| ]); | ||||
| 
 | ||||
| @customElement("btrix-table-header-cell") | ||||
| export class TableHeaderCell extends TableCell { | ||||
|   @property({ type: String, reflect: true, noAccessor: true }) | ||||
|   role = "columnheader"; | ||||
| 
 | ||||
|   @property({ type: String, reflect: true, noAccessor: true }) | ||||
|   ariaSort = "none"; | ||||
|   @property({ type: String, reflect: true }) | ||||
|   ariaSort: SortValues = "none"; | ||||
| 
 | ||||
|   render() { | ||||
|     return html`<slot></slot>`; | ||||
|     return html` <slot></slot> `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -218,7 +218,7 @@ export class ArchivedItemListItem extends TailwindElement { | ||||
|                 </label>` | ||||
|               : html`<div>${rowName}</div>`} | ||||
|         </btrix-table-cell> | ||||
|         <btrix-table-cell> | ||||
|         <btrix-table-cell class="tabular-nums"> | ||||
|           <sl-tooltip | ||||
|             content=${msg(str`By ${this.item.userName}`)} | ||||
|             @click=${this.onTooltipClick} | ||||
| @ -231,12 +231,10 @@ export class ArchivedItemListItem extends TailwindElement { | ||||
|               month="2-digit" | ||||
|               day="2-digit" | ||||
|               year="2-digit" | ||||
|               hour="2-digit" | ||||
|               minute="2-digit" | ||||
|             ></sl-format-date> | ||||
|           </sl-tooltip> | ||||
|         </btrix-table-cell> | ||||
|         <btrix-table-cell> | ||||
|         <btrix-table-cell class="tabular-nums"> | ||||
|           <sl-tooltip | ||||
|             hoist | ||||
|             content=${formatNumber(this.item.fileSize || 0, { | ||||
| @ -253,7 +251,7 @@ export class ArchivedItemListItem extends TailwindElement { | ||||
|             ></sl-format-bytes> | ||||
|           </sl-tooltip> | ||||
|         </btrix-table-cell> | ||||
|         <btrix-table-cell> | ||||
|         <btrix-table-cell class="tabular-nums"> | ||||
|           ${isUpload | ||||
|             ? notApplicable | ||||
|             : html`<sl-tooltip
 | ||||
| @ -275,7 +273,7 @@ export class ArchivedItemListItem extends TailwindElement { | ||||
|                 </div> | ||||
|               </sl-tooltip>`} | ||||
|         </btrix-table-cell> | ||||
|         <btrix-table-cell> | ||||
|         <btrix-table-cell class="tabular-nums"> | ||||
|           ${isUpload | ||||
|             ? notApplicable | ||||
|             : lastQAStarted && qaRunCount | ||||
| @ -387,7 +385,7 @@ export class ArchivedItemList extends TailwindElement { | ||||
|         </btrix-table-header-cell>`, | ||||
|       }, | ||||
|       { | ||||
|         cssCol: "12rem", | ||||
|         cssCol: "1fr", | ||||
|         cell: html`<btrix-table-header-cell>
 | ||||
|           ${msg("Date Created")} | ||||
|         </btrix-table-header-cell>`, | ||||
|  | ||||
| @ -80,6 +80,7 @@ export class BrowserProfilesDetail extends TailwindElement { | ||||
|   private readonly navigate = new NavigateController(this); | ||||
|   private readonly notify = new NotifyController(this); | ||||
| 
 | ||||
|   private readonly validateNameMax = maxLengthValidator(50); | ||||
|   private readonly validateDescriptionMax = maxLengthValidator(500); | ||||
| 
 | ||||
|   disconnectedCallback() { | ||||
| @ -139,7 +140,7 @@ export class BrowserProfilesDetail extends TailwindElement { | ||||
|                 : none | ||||
|               : nothing} | ||||
|           </btrix-desc-list-item> | ||||
|           <btrix-desc-list-item label=${msg("Created At")}> | ||||
|           <btrix-desc-list-item label=${msg("Created On")}> | ||||
|             ${this.profile | ||||
|               ? html` | ||||
|                   <sl-format-date | ||||
| @ -434,16 +435,17 @@ export class BrowserProfilesDetail extends TailwindElement { | ||||
|   private renderEditProfile() { | ||||
|     if (!this.profile) return; | ||||
| 
 | ||||
|     const { helpText, validate } = this.validateDescriptionMax; | ||||
| 
 | ||||
|     return html` | ||||
|       <form @submit=${this.onSubmitEdit}> | ||||
|         <div class="mb-5"> | ||||
|         <div> | ||||
|           <sl-input | ||||
|             name="name" | ||||
|             class="with-max-help-text" | ||||
|             label=${msg("Name")} | ||||
|             autocomplete="off" | ||||
|             value=${this.profile.name} | ||||
|             help-text=${this.validateNameMax.helpText} | ||||
|             @sl-input=${this.validateNameMax.validate} | ||||
|             required | ||||
|           ></sl-input> | ||||
|         </div> | ||||
| @ -451,13 +453,14 @@ export class BrowserProfilesDetail extends TailwindElement { | ||||
|         <div class="mb-5"> | ||||
|           <sl-textarea | ||||
|             name="description" | ||||
|             class="with-max-help-text" | ||||
|             label=${msg("Description")} | ||||
|             value=${this.profile.description || ""} | ||||
|             rows="3" | ||||
|             autocomplete="off" | ||||
|             resize="auto" | ||||
|             help-text=${helpText} | ||||
|             @sl-input=${validate} | ||||
|             help-text=${this.validateDescriptionMax.helpText} | ||||
|             @sl-input=${this.validateDescriptionMax.validate} | ||||
|           ></sl-textarea> | ||||
|         </div> | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { localized, msg, str } from "@lit/localize"; | ||||
| import { nothing, type PropertyValues } from "lit"; | ||||
| import clsx from "clsx"; | ||||
| import { css, nothing, type PropertyValues } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators.js"; | ||||
| import { when } from "lit/directives/when.js"; | ||||
| import queryString from "query-string"; | ||||
| @ -8,12 +9,25 @@ import type { Profile } from "./types"; | ||||
| 
 | ||||
| import type { SelectNewDialogEvent } from "."; | ||||
| 
 | ||||
| import { TailwindElement } from "@/classes/TailwindElement"; | ||||
| import type { PageChangeEvent } from "@/components/ui/pagination"; | ||||
| import type { APIPaginatedList, APIPaginationQuery } from "@/types/api"; | ||||
| import { | ||||
|   SortDirection, | ||||
|   type SortValues, | ||||
| } from "@/components/ui/table/table-header-cell"; | ||||
| import { APIController } from "@/controllers/api"; | ||||
| import { NavigateController } from "@/controllers/navigate"; | ||||
| import { NotifyController } from "@/controllers/notify"; | ||||
| import type { | ||||
|   APIPaginatedList, | ||||
|   APIPaginationQuery, | ||||
|   APISortQuery, | ||||
| } from "@/types/api"; | ||||
| import type { Browser } from "@/types/browser"; | ||||
| import type { AuthState } from "@/utils/AuthService"; | ||||
| import LiteElement, { html } from "@/utils/LiteElement"; | ||||
| import { html } from "@/utils/LiteElement"; | ||||
| import { getLocale } from "@/utils/localization"; | ||||
| import { tw } from "@/utils/tailwind"; | ||||
| 
 | ||||
| const INITIAL_PAGE_SIZE = 20; | ||||
| 
 | ||||
| @ -28,7 +42,7 @@ const INITIAL_PAGE_SIZE = 20; | ||||
|  */ | ||||
| @localized() | ||||
| @customElement("btrix-browser-profiles-list") | ||||
| export class BrowserProfilesList extends LiteElement { | ||||
| export class BrowserProfilesList extends TailwindElement { | ||||
|   @property({ type: Object }) | ||||
|   authState!: AuthState; | ||||
| 
 | ||||
| @ -41,10 +55,54 @@ export class BrowserProfilesList extends LiteElement { | ||||
|   @state() | ||||
|   browserProfiles?: APIPaginatedList<Profile>; | ||||
| 
 | ||||
|   @state() | ||||
|   sort: Required<APISortQuery> = { | ||||
|     sortBy: "modified", | ||||
|     sortDirection: -1, | ||||
|   }; | ||||
| 
 | ||||
|   @state() | ||||
|   private isLoading = true; | ||||
| 
 | ||||
|   static styles = css` | ||||
|     btrix-table { | ||||
|       grid-template-columns: | ||||
|         [clickable-start] minmax(30ch, 50ch) minmax(30ch, 40ch) repeat(2, 1fr) | ||||
|         [clickable-end] min-content; | ||||
|       --btrix-cell-gap: var(--sl-spacing-x-small); | ||||
|       --btrix-cell-padding-left: var(--sl-spacing-small); | ||||
|       --btrix-cell-padding-right: var(--sl-spacing-small); | ||||
|     } | ||||
| 
 | ||||
|     btrix-table-body btrix-table-row:nth-of-type(n + 2) { | ||||
|       --btrix-border-top: 1px solid var(--sl-panel-border-color); | ||||
|     } | ||||
| 
 | ||||
|     btrix-table-body btrix-table-row:first-of-type { | ||||
|       --btrix-border-radius-top: var(--sl-border-radius-medium); | ||||
|     } | ||||
| 
 | ||||
|     btrix-table-body btrix-table-row:last-of-type { | ||||
|       --btrix-border-radius-bottom: var(--sl-border-radius-medium); | ||||
|     } | ||||
| 
 | ||||
|     btrix-table-row { | ||||
|       border-top: var(--btrix-border-top, 0); | ||||
|       border-radius: var(--btrix-border-radius-top, 0) | ||||
|         var(--btrix-border-radius-to, 0) var(--btrix-border-radius-bottom, 0) | ||||
|         var(--btrix-border-radius-bottom, 0); | ||||
|       height: 2.5rem; | ||||
|     } | ||||
|   `;
 | ||||
| 
 | ||||
|   private readonly api = new APIController(this); | ||||
|   private readonly navigate = new NavigateController(this); | ||||
|   private readonly notify = new NotifyController(this); | ||||
| 
 | ||||
|   protected willUpdate( | ||||
|     changedProperties: PropertyValues<this> & Map<string, unknown>, | ||||
|   ) { | ||||
|     if (changedProperties.has("orgId")) { | ||||
|     if (changedProperties.has("orgId") || changedProperties.has("sort")) { | ||||
|       void this.fetchBrowserProfiles(); | ||||
|     } | ||||
|   } | ||||
| @ -76,43 +134,101 @@ export class BrowserProfilesList extends LiteElement { | ||||
|           )} | ||||
|         </div> | ||||
|       </header> | ||||
|       <div class="overflow-auto px-2 pb-1">${this.renderTable()}</div>`;
 | ||||
|       <div class="pb-1">${this.renderTable()}</div>`;
 | ||||
|   } | ||||
| 
 | ||||
|   private renderTable() { | ||||
|     const headerCells = [ | ||||
|       { | ||||
|         sortBy: "name", | ||||
|         sortDirection: 1, | ||||
|         className: "pl-3", | ||||
|         label: msg("Name"), | ||||
|       }, | ||||
|       { sortBy: "url", sortDirection: 1, label: msg("Visited URLs") }, | ||||
|       { sortBy: "created", sortDirection: -1, label: msg("Created On") }, | ||||
|       { sortBy: "modified", sortDirection: -1, label: msg("Last Updated") }, | ||||
|     ]; | ||||
|     const sortProps: Record< | ||||
|       SortValues, | ||||
|       { name: string; label: string; className: string } | ||||
|     > = { | ||||
|       none: { | ||||
|         name: "arrow-down-up", | ||||
|         label: msg("Sortable"), | ||||
|         className: tw`text-xs opacity-0 hover:opacity-100 group-hover:opacity-100`, | ||||
|       }, | ||||
|       ascending: { | ||||
|         name: "sort-up-alt", | ||||
|         label: msg("Ascending"), | ||||
|         className: tw`text-base`, | ||||
|       }, | ||||
|       descending: { | ||||
|         name: "sort-down", | ||||
|         label: msg("Descending"), | ||||
|         className: tw`text-base`, | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     const getSortIcon = (sortValue: SortValues) => { | ||||
|       const { name, label, className } = sortProps[sortValue]; | ||||
|       return html` | ||||
|       <btrix-table | ||||
|         style="grid-template-columns: [clickable-start] 60ch repeat(2, auto) [clickable-end] min-content; --btrix-cell-padding-left: var(--sl-spacing-x-small); --btrix-cell-padding-right: var(--sl-spacing-x-small);" | ||||
|       > | ||||
|         <sl-icon | ||||
|           class=${clsx(tw`ml-1 text-neutral-900 transition-opacity`, className)} | ||||
|           name=${name} | ||||
|           label=${label} | ||||
|         ></sl-icon> | ||||
|       `;
 | ||||
|     }; | ||||
| 
 | ||||
|     return html` | ||||
|       <btrix-table class="-mx-3 overflow-x-auto px-3"> | ||||
|         <btrix-table-head class="mb-2"> | ||||
|           <btrix-table-header-cell class="pl-3"> | ||||
|             ${msg("Name")} | ||||
|           </btrix-table-header-cell> | ||||
|           <btrix-table-header-cell> | ||||
|             ${msg("Visited URLs")} | ||||
|           </btrix-table-header-cell> | ||||
|           <btrix-table-header-cell> | ||||
|             ${msg("Last Updated")} | ||||
|           ${headerCells.map(({ sortBy, sortDirection, label, className }) => { | ||||
|             const isSorting = sortBy === this.sort.sortBy; | ||||
|             const sortValue = | ||||
|               (isSorting && SortDirection.get(this.sort.sortDirection)) || | ||||
|               "none"; | ||||
|             // TODO implement sort render logic in table-header-cell
 | ||||
|             return html` | ||||
|               <btrix-table-header-cell | ||||
|                 class="${className} group cursor-pointer rounded transition-colors hover:bg-primary-50" | ||||
|                 ariaSort=${sortValue} | ||||
|                 @click=${() => { | ||||
|                   if (isSorting) { | ||||
|                     this.sort = { | ||||
|                       ...this.sort, | ||||
|                       sortDirection: this.sort.sortDirection * -1, | ||||
|                     }; | ||||
|                   } else { | ||||
|                     this.sort = { | ||||
|                       sortBy, | ||||
|                       sortDirection, | ||||
|                     }; | ||||
|                   } | ||||
|                 }} | ||||
|               > | ||||
|                 ${label} ${getSortIcon(sortValue)} | ||||
|               </btrix-table-header-cell> | ||||
|             `;
 | ||||
|           })} | ||||
|           <btrix-table-header-cell> | ||||
|             <span class="sr-only">${msg("Row Actions")}</span> | ||||
|           </btrix-table-header-cell> | ||||
|         </btrix-table-head> | ||||
|         ${when(this.browserProfiles, ({ total, items }) => | ||||
|           total | ||||
|             ? html` | ||||
|         <btrix-table-body | ||||
|                   style="--btrix-row-gap: var(--sl-spacing-x-small); --btrix-cell-padding-top: var(--sl-spacing-2x-small); --btrix-cell-padding-bottom: var(--sl-spacing-2x-small);" | ||||
|                 > | ||||
|                   ${items.map(this.renderItem)} | ||||
|                 </btrix-table-body> | ||||
|               ` | ||||
|             : nothing, | ||||
|           class=${clsx( | ||||
|             "relative rounded border", | ||||
|             this.browserProfiles == null && this.isLoading && tw`min-h-48`, | ||||
|           )} | ||||
|         > | ||||
|           ${when(this.browserProfiles, ({ total, items }) => | ||||
|             total ? html` ${items.map(this.renderItem)} ` : nothing, | ||||
|           )} | ||||
|           ${when(this.isLoading, this.renderLoading)} | ||||
|         </btrix-table-body> | ||||
|       </btrix-table> | ||||
|       ${when( | ||||
|         this.browserProfiles, | ||||
|         ({ total, page, pageSize }) => | ||||
|       ${when(this.browserProfiles, ({ total, page, pageSize }) => | ||||
|         total | ||||
|           ? html` | ||||
|               <footer class="mt-6 flex justify-center"> | ||||
| @ -133,20 +249,21 @@ export class BrowserProfilesList extends LiteElement { | ||||
|                 </p> | ||||
|               </div> | ||||
|             `,
 | ||||
|         this.renderLoading, | ||||
|       )} | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   private readonly renderLoading = () => | ||||
|     html`<div class="my-24 flex w-full items-center justify-center text-3xl">
 | ||||
|     html`<div
 | ||||
|       class="absolute left-0 top-0 z-10 flex h-full w-full items-center justify-center bg-white/50 text-3xl" | ||||
|     > | ||||
|       <sl-spinner></sl-spinner> | ||||
|     </div>`;
 | ||||
| 
 | ||||
|   private readonly renderItem = (data: Profile) => { | ||||
|     return html` | ||||
|       <btrix-table-row | ||||
|         class="cursor-pointer select-none rounded border shadow transition-all focus-within:bg-neutral-50 hover:bg-neutral-50 hover:shadow-none" | ||||
|         class="cursor-pointer select-none transition-all focus-within:bg-neutral-50 hover:bg-neutral-50 hover:shadow-none" | ||||
|       > | ||||
|         <btrix-table-cell | ||||
|           class="flex-col items-center justify-center pl-3" | ||||
| @ -154,28 +271,45 @@ export class BrowserProfilesList extends LiteElement { | ||||
|         > | ||||
|           <a | ||||
|             class="flex items-center gap-3" | ||||
|             href=${`${this.orgBasePath}/browser-profiles/profile/${data.id}`} | ||||
|             @click=${this.navLink} | ||||
|             href=${`${this.navigate.orgBasePath}/browser-profiles/profile/${data.id}`} | ||||
|             @click=${this.navigate.link} | ||||
|           > | ||||
|             ${data.name} | ||||
|             <span class="truncate">${data.name}</span> | ||||
|           </a> | ||||
|         </btrix-table-cell> | ||||
|         <btrix-table-cell> | ||||
|           ${data.origins[0]}${data.origins.length > 1 | ||||
|             ? html`<sl-tooltip
 | ||||
|                 class="invert-tooltip" | ||||
|                 content=${data.origins.slice(1).join(", ")} | ||||
|           <div class="truncate">${data.origins[0]}</div> | ||||
|           ${data.origins.length > 1 | ||||
|             ? html`<sl-tooltip class="invert-tooltip">
 | ||||
|                 <span slot="content" class=" break-words" | ||||
|                   >${data.origins.slice(1).join(", ")}</span | ||||
|                 > | ||||
|                 <sl-tag size="small" class="ml-2"> | ||||
|                 <btrix-badge class="ml-2"> | ||||
|                   ${msg(str`+${data.origins.length - 1}`)} | ||||
|                 </sl-tag> | ||||
|                 </btrix-badge> | ||||
|               </sl-tooltip>` | ||||
|             : nothing} | ||||
|         </btrix-table-cell> | ||||
|         <btrix-table-cell class="whitespace-nowrap"> | ||||
|         <btrix-table-cell class="whitespace-nowrap tabular-nums"> | ||||
|           <sl-tooltip | ||||
|             content=${msg(str`By ${data.createdByName}`)} | ||||
|             ?disabled=${!data.createdByName} | ||||
|           > | ||||
|             <sl-format-date | ||||
|               lang=${getLocale()} | ||||
|               date=${`${data.created}Z` /** Z for UTC */} | ||||
|               month="2-digit" | ||||
|               day="2-digit" | ||||
|               year="2-digit" | ||||
|               hour="2-digit" | ||||
|               minute="2-digit" | ||||
|             ></sl-format-date> | ||||
|           </sl-tooltip> | ||||
|         </btrix-table-cell> | ||||
|         <btrix-table-cell class="whitespace-nowrap tabular-nums"> | ||||
|           <sl-tooltip | ||||
|             content=${msg(str`By ${data.modifiedByName || data.createdByName}`)} | ||||
|             ?disabled=${!(data.modifiedByName || data.createdByName)} | ||||
|             ?disabled=${!data.createdByName} | ||||
|           > | ||||
|             <sl-format-date | ||||
|               lang=${getLocale()} | ||||
| @ -232,14 +366,14 @@ export class BrowserProfilesList extends LiteElement { | ||||
|     try { | ||||
|       const data = await this.createBrowser({ url }); | ||||
| 
 | ||||
|       this.notify({ | ||||
|       this.notify.toast({ | ||||
|         message: msg("Starting up browser with selected profile..."), | ||||
|         variant: "success", | ||||
|         icon: "check2-circle", | ||||
|       }); | ||||
| 
 | ||||
|       this.navTo( | ||||
|         `${this.orgBasePath}/browser-profiles/profile/browser/${ | ||||
|       this.navigate.to( | ||||
|         `${this.navigate.orgBasePath}/browser-profiles/profile/browser/${ | ||||
|           data.browserid | ||||
|         }?${queryString.stringify({ | ||||
|           url, | ||||
| @ -250,7 +384,7 @@ export class BrowserProfilesList extends LiteElement { | ||||
|         })}`,
 | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       this.notify({ | ||||
|       this.notify.toast({ | ||||
|         message: msg("Sorry, couldn't create browser profile at this time."), | ||||
|         variant: "danger", | ||||
|         icon: "exclamation-octagon", | ||||
| @ -260,7 +394,7 @@ export class BrowserProfilesList extends LiteElement { | ||||
| 
 | ||||
|   private async deleteProfile(profile: Profile) { | ||||
|     try { | ||||
|       const data = await this.apiFetch<Profile & { error?: boolean }>( | ||||
|       const data = await this.api.fetch<Profile & { error?: boolean }>( | ||||
|         `/orgs/${this.orgId}/profiles/${profile.id}`, | ||||
|         this.authState!, | ||||
|         { | ||||
| @ -269,7 +403,7 @@ export class BrowserProfilesList extends LiteElement { | ||||
|       ); | ||||
| 
 | ||||
|       if (data.error && data.crawlconfigs) { | ||||
|         this.notify({ | ||||
|         this.notify.toast({ | ||||
|           message: msg( | ||||
|             html`Could not delete <strong>${profile.name}</strong>, in use by
 | ||||
|               <strong | ||||
| @ -281,7 +415,7 @@ export class BrowserProfilesList extends LiteElement { | ||||
|           duration: 15000, | ||||
|         }); | ||||
|       } else { | ||||
|         this.notify({ | ||||
|         this.notify.toast({ | ||||
|           message: msg(html`Deleted <strong>${profile.name}</strong>.`), | ||||
|           variant: "success", | ||||
|           icon: "check2-circle", | ||||
| @ -290,7 +424,7 @@ export class BrowserProfilesList extends LiteElement { | ||||
|         void this.fetchBrowserProfiles(); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       this.notify({ | ||||
|       this.notify.toast({ | ||||
|         message: msg("Sorry, couldn't delete browser profile at this time."), | ||||
|         variant: "danger", | ||||
|         icon: "exclamation-octagon", | ||||
| @ -303,7 +437,7 @@ export class BrowserProfilesList extends LiteElement { | ||||
|       url, | ||||
|     }; | ||||
| 
 | ||||
|     return this.apiFetch<Browser>( | ||||
|     return this.api.fetch<Browser>( | ||||
|       `/orgs/${this.orgId}/profiles/browser`, | ||||
|       this.authState!, | ||||
|       { | ||||
| @ -320,6 +454,7 @@ export class BrowserProfilesList extends LiteElement { | ||||
|     params?: APIPaginationQuery, | ||||
|   ): Promise<void> { | ||||
|     try { | ||||
|       this.isLoading = true; | ||||
|       const data = await this.getProfiles({ | ||||
|         page: params?.page || this.browserProfiles?.page || 1, | ||||
|         pageSize: | ||||
| @ -330,20 +465,28 @@ export class BrowserProfilesList extends LiteElement { | ||||
| 
 | ||||
|       this.browserProfiles = data; | ||||
|     } catch (e) { | ||||
|       this.notify({ | ||||
|       this.notify.toast({ | ||||
|         message: msg("Sorry, couldn't retrieve browser profiles at this time."), | ||||
|         variant: "danger", | ||||
|         icon: "exclamation-octagon", | ||||
|       }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async getProfiles(params: APIPaginationQuery) { | ||||
|     const query = queryString.stringify(params, { | ||||
|     const query = queryString.stringify( | ||||
|       { | ||||
|         ...params, | ||||
|         ...this.sort, | ||||
|       }, | ||||
|       { | ||||
|         arrayFormat: "comma", | ||||
|     }); | ||||
|       }, | ||||
|     ); | ||||
| 
 | ||||
|     const data = await this.apiFetch<APIPaginatedList<Profile>>( | ||||
|     const data = await this.api.fetch<APIPaginatedList<Profile>>( | ||||
|       `/orgs/${this.orgId}/profiles?${query}`, | ||||
|       this.authState!, | ||||
|     ); | ||||
|  | ||||
| @ -400,7 +400,7 @@ export class CollectionsList extends LiteElement { | ||||
|     if (this.collections?.items.length) { | ||||
|       return html` | ||||
|         <btrix-table | ||||
|           style="grid-template-columns: min-content [clickable-start] 60ch repeat(3, 1fr) 12rem [clickable-end] min-content" | ||||
|           style="grid-template-columns: min-content [clickable-start] 50ch repeat(4, 1fr) [clickable-end] min-content" | ||||
|         > | ||||
|           <btrix-table-head class="mb-2"> | ||||
|             <btrix-table-header-cell> | ||||
|  | ||||
| @ -51,7 +51,7 @@ const sortableFields: Record< | ||||
|     defaultDirection: "asc", | ||||
|   }, | ||||
|   created: { | ||||
|     label: msg("Created"), | ||||
|     label: msg("Date Created"), | ||||
|     defaultDirection: "desc", | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| import type { SortDirection } from "./utils"; | ||||
| 
 | ||||
| /** | ||||
|  * If no generic type is specified, `items` cannot exist. | ||||
|  */ | ||||
| @ -21,5 +23,5 @@ export type APIPaginationQuery = { | ||||
| 
 | ||||
| export type APISortQuery = { | ||||
|   sortBy?: string; | ||||
|   sortDirection?: number; // -1 | 1
 | ||||
|   sortDirection?: SortDirection; | ||||
| }; | ||||
|  | ||||
| @ -21,3 +21,6 @@ export type Range<F extends number, T extends number> = Exclude< | ||||
|   Enumerate<T>, | ||||
|   Enumerate<F> | ||||
| >; | ||||
| 
 | ||||
| /** 1 or -1, but will accept any number for easier typing where this is used **/ | ||||
| export type SortDirection = -1 | 1 | (number & {}); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user