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}]
|
aggregate: List[Dict[str, Any]] = [{"$match": match_query}]
|
||||||
|
|
||||||
if sort_by:
|
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")
|
raise HTTPException(status_code=400, detail="invalid_sort_by")
|
||||||
if sort_direction not in (1, -1):
|
if sort_direction not in (1, -1):
|
||||||
raise HTTPException(status_code=400, detail="invalid_sort_direction")
|
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([{"$sort": {sort_by: sort_direction}}])
|
||||||
|
|
||||||
aggregate.extend(
|
aggregate.extend(
|
||||||
|
|||||||
@ -512,7 +512,9 @@ def profile_browser_id(admin_auth_headers, default_org_id):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def profile_browser_2_id(admin_auth_headers, default_org_id):
|
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")
|
@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)
|
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(
|
r = requests.post(
|
||||||
f"{API_PREFIX}/orgs/{oid}/profiles/browser",
|
f"{API_PREFIX}/orgs/{oid}/profiles/browser",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"url": "https://webrecorder.net"},
|
json={"url": url},
|
||||||
)
|
)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
browser_id = r.json()["browserid"]
|
browser_id = r.json()["browserid"]
|
||||||
|
|||||||
@ -432,6 +432,10 @@ def test_commit_browser_to_existing_profile(
|
|||||||
("name", -1, 0, 1),
|
("name", -1, 0, 1),
|
||||||
# Name, ascending
|
# Name, ascending
|
||||||
("name", 1, 1, 0),
|
("name", 1, 1, 0),
|
||||||
|
# URL, descending
|
||||||
|
("url", -1, 0, 1),
|
||||||
|
# URL, ascending
|
||||||
|
("url", 1, 1, 0),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_sort_profiles(
|
def test_sort_profiles(
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { css, html, LitElement } from "lit";
|
import { css, html } from "lit";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
property,
|
property,
|
||||||
queryAssignedElements,
|
queryAssignedElements,
|
||||||
} from "lit/decorators.js";
|
} from "lit/decorators.js";
|
||||||
|
|
||||||
|
import { TailwindElement } from "@/classes/TailwindElement";
|
||||||
|
|
||||||
export const ALLOWED_ROW_CLICK_TARGET_TAG = ["a", "label"] as const;
|
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
|
* @cssproperty --btrix-cell-padding-bottom
|
||||||
*/
|
*/
|
||||||
@customElement("btrix-table-cell")
|
@customElement("btrix-table-cell")
|
||||||
export class TableCell extends LitElement {
|
export class TableCell extends TailwindElement {
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -3,15 +3,23 @@ import { customElement, property } from "lit/decorators.js";
|
|||||||
|
|
||||||
import { TableCell } from "./table-cell";
|
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")
|
@customElement("btrix-table-header-cell")
|
||||||
export class TableHeaderCell extends TableCell {
|
export class TableHeaderCell extends TableCell {
|
||||||
@property({ type: String, reflect: true, noAccessor: true })
|
@property({ type: String, reflect: true, noAccessor: true })
|
||||||
role = "columnheader";
|
role = "columnheader";
|
||||||
|
|
||||||
@property({ type: String, reflect: true, noAccessor: true })
|
@property({ type: String, reflect: true })
|
||||||
ariaSort = "none";
|
ariaSort: SortValues = "none";
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`<slot></slot>`;
|
return html` <slot></slot> `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -218,7 +218,7 @@ export class ArchivedItemListItem extends TailwindElement {
|
|||||||
</label>`
|
</label>`
|
||||||
: html`<div>${rowName}</div>`}
|
: html`<div>${rowName}</div>`}
|
||||||
</btrix-table-cell>
|
</btrix-table-cell>
|
||||||
<btrix-table-cell>
|
<btrix-table-cell class="tabular-nums">
|
||||||
<sl-tooltip
|
<sl-tooltip
|
||||||
content=${msg(str`By ${this.item.userName}`)}
|
content=${msg(str`By ${this.item.userName}`)}
|
||||||
@click=${this.onTooltipClick}
|
@click=${this.onTooltipClick}
|
||||||
@ -231,12 +231,10 @@ export class ArchivedItemListItem extends TailwindElement {
|
|||||||
month="2-digit"
|
month="2-digit"
|
||||||
day="2-digit"
|
day="2-digit"
|
||||||
year="2-digit"
|
year="2-digit"
|
||||||
hour="2-digit"
|
|
||||||
minute="2-digit"
|
|
||||||
></sl-format-date>
|
></sl-format-date>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
</btrix-table-cell>
|
</btrix-table-cell>
|
||||||
<btrix-table-cell>
|
<btrix-table-cell class="tabular-nums">
|
||||||
<sl-tooltip
|
<sl-tooltip
|
||||||
hoist
|
hoist
|
||||||
content=${formatNumber(this.item.fileSize || 0, {
|
content=${formatNumber(this.item.fileSize || 0, {
|
||||||
@ -253,7 +251,7 @@ export class ArchivedItemListItem extends TailwindElement {
|
|||||||
></sl-format-bytes>
|
></sl-format-bytes>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
</btrix-table-cell>
|
</btrix-table-cell>
|
||||||
<btrix-table-cell>
|
<btrix-table-cell class="tabular-nums">
|
||||||
${isUpload
|
${isUpload
|
||||||
? notApplicable
|
? notApplicable
|
||||||
: html`<sl-tooltip
|
: html`<sl-tooltip
|
||||||
@ -275,7 +273,7 @@ export class ArchivedItemListItem extends TailwindElement {
|
|||||||
</div>
|
</div>
|
||||||
</sl-tooltip>`}
|
</sl-tooltip>`}
|
||||||
</btrix-table-cell>
|
</btrix-table-cell>
|
||||||
<btrix-table-cell>
|
<btrix-table-cell class="tabular-nums">
|
||||||
${isUpload
|
${isUpload
|
||||||
? notApplicable
|
? notApplicable
|
||||||
: lastQAStarted && qaRunCount
|
: lastQAStarted && qaRunCount
|
||||||
@ -387,7 +385,7 @@ export class ArchivedItemList extends TailwindElement {
|
|||||||
</btrix-table-header-cell>`,
|
</btrix-table-header-cell>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cssCol: "12rem",
|
cssCol: "1fr",
|
||||||
cell: html`<btrix-table-header-cell>
|
cell: html`<btrix-table-header-cell>
|
||||||
${msg("Date Created")}
|
${msg("Date Created")}
|
||||||
</btrix-table-header-cell>`,
|
</btrix-table-header-cell>`,
|
||||||
|
|||||||
@ -80,6 +80,7 @@ export class BrowserProfilesDetail extends TailwindElement {
|
|||||||
private readonly navigate = new NavigateController(this);
|
private readonly navigate = new NavigateController(this);
|
||||||
private readonly notify = new NotifyController(this);
|
private readonly notify = new NotifyController(this);
|
||||||
|
|
||||||
|
private readonly validateNameMax = maxLengthValidator(50);
|
||||||
private readonly validateDescriptionMax = maxLengthValidator(500);
|
private readonly validateDescriptionMax = maxLengthValidator(500);
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
@ -139,7 +140,7 @@ export class BrowserProfilesDetail extends TailwindElement {
|
|||||||
: none
|
: none
|
||||||
: nothing}
|
: nothing}
|
||||||
</btrix-desc-list-item>
|
</btrix-desc-list-item>
|
||||||
<btrix-desc-list-item label=${msg("Created At")}>
|
<btrix-desc-list-item label=${msg("Created On")}>
|
||||||
${this.profile
|
${this.profile
|
||||||
? html`
|
? html`
|
||||||
<sl-format-date
|
<sl-format-date
|
||||||
@ -434,16 +435,17 @@ export class BrowserProfilesDetail extends TailwindElement {
|
|||||||
private renderEditProfile() {
|
private renderEditProfile() {
|
||||||
if (!this.profile) return;
|
if (!this.profile) return;
|
||||||
|
|
||||||
const { helpText, validate } = this.validateDescriptionMax;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<form @submit=${this.onSubmitEdit}>
|
<form @submit=${this.onSubmitEdit}>
|
||||||
<div class="mb-5">
|
<div>
|
||||||
<sl-input
|
<sl-input
|
||||||
name="name"
|
name="name"
|
||||||
|
class="with-max-help-text"
|
||||||
label=${msg("Name")}
|
label=${msg("Name")}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
value=${this.profile.name}
|
value=${this.profile.name}
|
||||||
|
help-text=${this.validateNameMax.helpText}
|
||||||
|
@sl-input=${this.validateNameMax.validate}
|
||||||
required
|
required
|
||||||
></sl-input>
|
></sl-input>
|
||||||
</div>
|
</div>
|
||||||
@ -451,13 +453,14 @@ export class BrowserProfilesDetail extends TailwindElement {
|
|||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<sl-textarea
|
<sl-textarea
|
||||||
name="description"
|
name="description"
|
||||||
|
class="with-max-help-text"
|
||||||
label=${msg("Description")}
|
label=${msg("Description")}
|
||||||
value=${this.profile.description || ""}
|
value=${this.profile.description || ""}
|
||||||
rows="3"
|
rows="3"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
resize="auto"
|
resize="auto"
|
||||||
help-text=${helpText}
|
help-text=${this.validateDescriptionMax.helpText}
|
||||||
@sl-input=${validate}
|
@sl-input=${this.validateDescriptionMax.validate}
|
||||||
></sl-textarea>
|
></sl-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { localized, msg, str } from "@lit/localize";
|
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 { customElement, property, state } from "lit/decorators.js";
|
||||||
import { when } from "lit/directives/when.js";
|
import { when } from "lit/directives/when.js";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
@ -8,12 +9,25 @@ import type { Profile } from "./types";
|
|||||||
|
|
||||||
import type { SelectNewDialogEvent } from ".";
|
import type { SelectNewDialogEvent } from ".";
|
||||||
|
|
||||||
|
import { TailwindElement } from "@/classes/TailwindElement";
|
||||||
import type { PageChangeEvent } from "@/components/ui/pagination";
|
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 { Browser } from "@/types/browser";
|
||||||
import type { AuthState } from "@/utils/AuthService";
|
import type { AuthState } from "@/utils/AuthService";
|
||||||
import LiteElement, { html } from "@/utils/LiteElement";
|
import { html } from "@/utils/LiteElement";
|
||||||
import { getLocale } from "@/utils/localization";
|
import { getLocale } from "@/utils/localization";
|
||||||
|
import { tw } from "@/utils/tailwind";
|
||||||
|
|
||||||
const INITIAL_PAGE_SIZE = 20;
|
const INITIAL_PAGE_SIZE = 20;
|
||||||
|
|
||||||
@ -28,7 +42,7 @@ const INITIAL_PAGE_SIZE = 20;
|
|||||||
*/
|
*/
|
||||||
@localized()
|
@localized()
|
||||||
@customElement("btrix-browser-profiles-list")
|
@customElement("btrix-browser-profiles-list")
|
||||||
export class BrowserProfilesList extends LiteElement {
|
export class BrowserProfilesList extends TailwindElement {
|
||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
authState!: AuthState;
|
authState!: AuthState;
|
||||||
|
|
||||||
@ -41,10 +55,54 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
@state()
|
@state()
|
||||||
browserProfiles?: APIPaginatedList<Profile>;
|
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(
|
protected willUpdate(
|
||||||
changedProperties: PropertyValues<this> & Map<string, unknown>,
|
changedProperties: PropertyValues<this> & Map<string, unknown>,
|
||||||
) {
|
) {
|
||||||
if (changedProperties.has("orgId")) {
|
if (changedProperties.has("orgId") || changedProperties.has("sort")) {
|
||||||
void this.fetchBrowserProfiles();
|
void this.fetchBrowserProfiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,77 +134,136 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="overflow-auto px-2 pb-1">${this.renderTable()}</div>`;
|
<div class="pb-1">${this.renderTable()}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderTable() {
|
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`
|
||||||
|
<sl-icon
|
||||||
|
class=${clsx(tw`ml-1 text-neutral-900 transition-opacity`, className)}
|
||||||
|
name=${name}
|
||||||
|
label=${label}
|
||||||
|
></sl-icon>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<btrix-table
|
<btrix-table class="-mx-3 overflow-x-auto px-3">
|
||||||
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);"
|
|
||||||
>
|
|
||||||
<btrix-table-head class="mb-2">
|
<btrix-table-head class="mb-2">
|
||||||
<btrix-table-header-cell class="pl-3">
|
${headerCells.map(({ sortBy, sortDirection, label, className }) => {
|
||||||
${msg("Name")}
|
const isSorting = sortBy === this.sort.sortBy;
|
||||||
</btrix-table-header-cell>
|
const sortValue =
|
||||||
<btrix-table-header-cell>
|
(isSorting && SortDirection.get(this.sort.sortDirection)) ||
|
||||||
${msg("Visited URLs")}
|
"none";
|
||||||
</btrix-table-header-cell>
|
// TODO implement sort render logic in table-header-cell
|
||||||
<btrix-table-header-cell>
|
return html`
|
||||||
${msg("Last Updated")}
|
<btrix-table-header-cell
|
||||||
</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>
|
<btrix-table-header-cell>
|
||||||
<span class="sr-only">${msg("Row Actions")}</span>
|
<span class="sr-only">${msg("Row Actions")}</span>
|
||||||
</btrix-table-header-cell>
|
</btrix-table-header-cell>
|
||||||
</btrix-table-head>
|
</btrix-table-head>
|
||||||
${when(this.browserProfiles, ({ total, items }) =>
|
<btrix-table-body
|
||||||
total
|
class=${clsx(
|
||||||
? html`
|
"relative rounded border",
|
||||||
<btrix-table-body
|
this.browserProfiles == null && this.isLoading && tw`min-h-48`,
|
||||||
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)}
|
${when(this.browserProfiles, ({ total, items }) =>
|
||||||
</btrix-table-body>
|
total ? html` ${items.map(this.renderItem)} ` : nothing,
|
||||||
`
|
)}
|
||||||
: nothing,
|
${when(this.isLoading, this.renderLoading)}
|
||||||
)}
|
</btrix-table-body>
|
||||||
</btrix-table>
|
</btrix-table>
|
||||||
${when(
|
${when(this.browserProfiles, ({ total, page, pageSize }) =>
|
||||||
this.browserProfiles,
|
total
|
||||||
({ total, page, pageSize }) =>
|
? html`
|
||||||
total
|
<footer class="mt-6 flex justify-center">
|
||||||
? html`
|
<btrix-pagination
|
||||||
<footer class="mt-6 flex justify-center">
|
page=${page}
|
||||||
<btrix-pagination
|
totalCount=${total}
|
||||||
page=${page}
|
size=${pageSize}
|
||||||
totalCount=${total}
|
@page-change=${async (e: PageChangeEvent) => {
|
||||||
size=${pageSize}
|
void this.fetchBrowserProfiles({ page: e.detail.page });
|
||||||
@page-change=${async (e: PageChangeEvent) => {
|
}}
|
||||||
void this.fetchBrowserProfiles({ page: e.detail.page });
|
></btrix-pagination>
|
||||||
}}
|
</footer>
|
||||||
></btrix-pagination>
|
`
|
||||||
</footer>
|
: html`
|
||||||
`
|
<div class="border-b border-t py-5">
|
||||||
: html`
|
<p class="text-center text-0-500">
|
||||||
<div class="border-b border-t py-5">
|
${msg("No browser profiles yet.")}
|
||||||
<p class="text-center text-0-500">
|
</p>
|
||||||
${msg("No browser profiles yet.")}
|
</div>
|
||||||
</p>
|
`,
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
this.renderLoading,
|
|
||||||
)}
|
)}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly 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>
|
<sl-spinner></sl-spinner>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
private readonly renderItem = (data: Profile) => {
|
private readonly renderItem = (data: Profile) => {
|
||||||
return html`
|
return html`
|
||||||
<btrix-table-row
|
<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
|
<btrix-table-cell
|
||||||
class="flex-col items-center justify-center pl-3"
|
class="flex-col items-center justify-center pl-3"
|
||||||
@ -154,28 +271,45 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="flex items-center gap-3"
|
class="flex items-center gap-3"
|
||||||
href=${`${this.orgBasePath}/browser-profiles/profile/${data.id}`}
|
href=${`${this.navigate.orgBasePath}/browser-profiles/profile/${data.id}`}
|
||||||
@click=${this.navLink}
|
@click=${this.navigate.link}
|
||||||
>
|
>
|
||||||
${data.name}
|
<span class="truncate">${data.name}</span>
|
||||||
</a>
|
</a>
|
||||||
</btrix-table-cell>
|
</btrix-table-cell>
|
||||||
<btrix-table-cell>
|
<btrix-table-cell>
|
||||||
${data.origins[0]}${data.origins.length > 1
|
<div class="truncate">${data.origins[0]}</div>
|
||||||
? html`<sl-tooltip
|
${data.origins.length > 1
|
||||||
class="invert-tooltip"
|
? html`<sl-tooltip class="invert-tooltip">
|
||||||
content=${data.origins.slice(1).join(", ")}
|
<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}`)}
|
${msg(str`+${data.origins.length - 1}`)}
|
||||||
</sl-tag>
|
</btrix-badge>
|
||||||
</sl-tooltip>`
|
</sl-tooltip>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</btrix-table-cell>
|
</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
|
<sl-tooltip
|
||||||
content=${msg(str`By ${data.modifiedByName || data.createdByName}`)}
|
content=${msg(str`By ${data.modifiedByName || data.createdByName}`)}
|
||||||
?disabled=${!(data.modifiedByName || data.createdByName)}
|
?disabled=${!data.createdByName}
|
||||||
>
|
>
|
||||||
<sl-format-date
|
<sl-format-date
|
||||||
lang=${getLocale()}
|
lang=${getLocale()}
|
||||||
@ -232,14 +366,14 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
try {
|
try {
|
||||||
const data = await this.createBrowser({ url });
|
const data = await this.createBrowser({ url });
|
||||||
|
|
||||||
this.notify({
|
this.notify.toast({
|
||||||
message: msg("Starting up browser with selected profile..."),
|
message: msg("Starting up browser with selected profile..."),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
icon: "check2-circle",
|
icon: "check2-circle",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.navTo(
|
this.navigate.to(
|
||||||
`${this.orgBasePath}/browser-profiles/profile/browser/${
|
`${this.navigate.orgBasePath}/browser-profiles/profile/browser/${
|
||||||
data.browserid
|
data.browserid
|
||||||
}?${queryString.stringify({
|
}?${queryString.stringify({
|
||||||
url,
|
url,
|
||||||
@ -250,7 +384,7 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.notify({
|
this.notify.toast({
|
||||||
message: msg("Sorry, couldn't create browser profile at this time."),
|
message: msg("Sorry, couldn't create browser profile at this time."),
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
icon: "exclamation-octagon",
|
icon: "exclamation-octagon",
|
||||||
@ -260,7 +394,7 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
|
|
||||||
private async deleteProfile(profile: Profile) {
|
private async deleteProfile(profile: Profile) {
|
||||||
try {
|
try {
|
||||||
const data = await this.apiFetch<Profile & { error?: boolean }>(
|
const data = await this.api.fetch<Profile & { error?: boolean }>(
|
||||||
`/orgs/${this.orgId}/profiles/${profile.id}`,
|
`/orgs/${this.orgId}/profiles/${profile.id}`,
|
||||||
this.authState!,
|
this.authState!,
|
||||||
{
|
{
|
||||||
@ -269,7 +403,7 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (data.error && data.crawlconfigs) {
|
if (data.error && data.crawlconfigs) {
|
||||||
this.notify({
|
this.notify.toast({
|
||||||
message: msg(
|
message: msg(
|
||||||
html`Could not delete <strong>${profile.name}</strong>, in use by
|
html`Could not delete <strong>${profile.name}</strong>, in use by
|
||||||
<strong
|
<strong
|
||||||
@ -281,7 +415,7 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
duration: 15000,
|
duration: 15000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.notify({
|
this.notify.toast({
|
||||||
message: msg(html`Deleted <strong>${profile.name}</strong>.`),
|
message: msg(html`Deleted <strong>${profile.name}</strong>.`),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
icon: "check2-circle",
|
icon: "check2-circle",
|
||||||
@ -290,7 +424,7 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
void this.fetchBrowserProfiles();
|
void this.fetchBrowserProfiles();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.notify({
|
this.notify.toast({
|
||||||
message: msg("Sorry, couldn't delete browser profile at this time."),
|
message: msg("Sorry, couldn't delete browser profile at this time."),
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
icon: "exclamation-octagon",
|
icon: "exclamation-octagon",
|
||||||
@ -303,7 +437,7 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
url,
|
url,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.apiFetch<Browser>(
|
return this.api.fetch<Browser>(
|
||||||
`/orgs/${this.orgId}/profiles/browser`,
|
`/orgs/${this.orgId}/profiles/browser`,
|
||||||
this.authState!,
|
this.authState!,
|
||||||
{
|
{
|
||||||
@ -320,6 +454,7 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
params?: APIPaginationQuery,
|
params?: APIPaginationQuery,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
const data = await this.getProfiles({
|
const data = await this.getProfiles({
|
||||||
page: params?.page || this.browserProfiles?.page || 1,
|
page: params?.page || this.browserProfiles?.page || 1,
|
||||||
pageSize:
|
pageSize:
|
||||||
@ -330,20 +465,28 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
|
|
||||||
this.browserProfiles = data;
|
this.browserProfiles = data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.notify({
|
this.notify.toast({
|
||||||
message: msg("Sorry, couldn't retrieve browser profiles at this time."),
|
message: msg("Sorry, couldn't retrieve browser profiles at this time."),
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
icon: "exclamation-octagon",
|
icon: "exclamation-octagon",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getProfiles(params: APIPaginationQuery) {
|
private async getProfiles(params: APIPaginationQuery) {
|
||||||
const query = queryString.stringify(params, {
|
const query = queryString.stringify(
|
||||||
arrayFormat: "comma",
|
{
|
||||||
});
|
...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}`,
|
`/orgs/${this.orgId}/profiles?${query}`,
|
||||||
this.authState!,
|
this.authState!,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -400,7 +400,7 @@ export class CollectionsList extends LiteElement {
|
|||||||
if (this.collections?.items.length) {
|
if (this.collections?.items.length) {
|
||||||
return html`
|
return html`
|
||||||
<btrix-table
|
<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-head class="mb-2">
|
||||||
<btrix-table-header-cell>
|
<btrix-table-header-cell>
|
||||||
|
|||||||
@ -51,7 +51,7 @@ const sortableFields: Record<
|
|||||||
defaultDirection: "asc",
|
defaultDirection: "asc",
|
||||||
},
|
},
|
||||||
created: {
|
created: {
|
||||||
label: msg("Created"),
|
label: msg("Date Created"),
|
||||||
defaultDirection: "desc",
|
defaultDirection: "desc",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import type { SortDirection } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If no generic type is specified, `items` cannot exist.
|
* If no generic type is specified, `items` cannot exist.
|
||||||
*/
|
*/
|
||||||
@ -21,5 +23,5 @@ export type APIPaginationQuery = {
|
|||||||
|
|
||||||
export type APISortQuery = {
|
export type APISortQuery = {
|
||||||
sortBy?: string;
|
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<T>,
|
||||||
Enumerate<F>
|
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